From 021b0c618b355e8c3c586ba56101b098d1697b7e Mon Sep 17 00:00:00 2001 From: super <2903208875@qq.com> Date: Tue, 16 Jun 2026 22:19:55 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=9B=BE=E7=89=87=E4=B8=8B?= =?UTF-8?q?=E8=BD=BD=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PermissionMenuSchemaInitializer.java | 3 +- .../service/PriceTrackTaskService.java | 8 +- .../ProductRiskExcelAssemblyService.java | 28 ++++- .../util/SimilarAsinImageEmbedder.java | 63 +++++++++- .../db/V60__split_version_admin_menu.sql | 25 ++++ .../ProductRiskExcelAssemblyServiceTest.java | 58 +++++++++ .../util/SimilarAsinImageEmbedderTest.java | 19 +++ backend/blueprints/admin_api.py | 18 ++- backend/config.py | 4 +- backend/static/admin.js | 115 +++++++++++++++--- backend/web_source/admin.html | 60 +++++++-- 11 files changed, 358 insertions(+), 43 deletions(-) create mode 100644 backend-java/src/main/resources/db/V60__split_version_admin_menu.sql create mode 100644 backend-java/src/test/java/com/nanri/aiimage/modules/productrisk/service/ProductRiskExcelAssemblyServiceTest.java diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/permission/service/PermissionMenuSchemaInitializer.java b/backend-java/src/main/java/com/nanri/aiimage/modules/permission/service/PermissionMenuSchemaInitializer.java index ccd77e0..bec699d 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/permission/service/PermissionMenuSchemaInitializer.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/permission/service/PermissionMenuSchemaInitializer.java @@ -36,7 +36,8 @@ public class PermissionMenuSchemaInitializer { new DefaultAdminMenu("查询ASIN", "admin_query_asin", "query-asin", 65), new DefaultAdminMenu("商品类目", "admin_product_categories", "product-categories", 66), 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) diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/service/PriceTrackTaskService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/service/PriceTrackTaskService.java index f0bf512..544ff7b 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/service/PriceTrackTaskService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/service/PriceTrackTaskService.java @@ -78,7 +78,7 @@ public class PriceTrackTaskService { private FileTaskEntity loadTaskForExecution(Long taskId) { Map cachedTasks = priceTrackTaskCacheService.getTaskCacheBatch(List.of(taskId)); FileTaskEntity cached = cachedTasks.get(taskId); - if (cached != null) { + if (cached != null && hasText(cached.getRequestJson())) { return cached; } FileTaskEntity dbTask = fileTaskMapper.selectById(taskId); @@ -396,7 +396,6 @@ public class PriceTrackTaskService { task.setCreatedAt(LocalDateTime.now()); task.setUpdatedAt(LocalDateTime.now()); fileTaskMapper.insert(task); - priceTrackTaskCacheService.saveTaskCache(task); priceTrackTaskCacheService.touchTaskHeartbeat(task.getId()); if (request.getLoopRunId() != null) { priceTrackLoopRunService.bindChildTask(request.getLoopRunId(), task.getId(), request.getRoundIndex(), request.getShopIndex()); @@ -447,6 +446,7 @@ public class PriceTrackTaskService { throw new BusinessException("序列化任务数据失败"); } fileTaskMapper.updateById(task); + priceTrackTaskCacheService.saveTaskCache(task); // TODO: Python 侧轮询 RUNNING 任务,pickup 后开始处理 // Java 侧通过 submitResult 接口接收 Python 回传结果 @@ -1548,6 +1548,10 @@ public class PriceTrackTaskService { return taskDistributedLockService.acquire(MODULE_TYPE, taskId); } + private static boolean hasText(String value) { + return value != null && !value.isBlank(); + } + private void cleanupTaskCacheIfTerminal(Long taskId, String taskStatus) { if (!"SUCCESS".equals(taskStatus) && !"FAILED".equals(taskStatus) && !"DELETE_EMPTY".equals(taskStatus)) { return; diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/productrisk/service/ProductRiskExcelAssemblyService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/productrisk/service/ProductRiskExcelAssemblyService.java index 1aea1cb..c2411b5 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/productrisk/service/ProductRiskExcelAssemblyService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/productrisk/service/ProductRiskExcelAssemblyService.java @@ -51,7 +51,7 @@ public class ProductRiskExcelAssemblyService { createTextCell(row, 0, firstNonBlank(dto == null ? null : dto.getShopName(), shopDisplayName)); createTextCell(row, 1, dto == null ? null : dto.getProductAsinSku()); 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, 5, dto == null ? null : dto.getRemoveStatus()); } @@ -119,6 +119,32 @@ public class ProductRiskExcelAssemblyService { || 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) { return value != null && !value.isBlank(); } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/util/SimilarAsinImageEmbedder.java b/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/util/SimilarAsinImageEmbedder.java index fa51f5c..54763bb 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/util/SimilarAsinImageEmbedder.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/util/SimilarAsinImageEmbedder.java @@ -35,6 +35,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; +import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; @@ -345,13 +346,20 @@ public class SimilarAsinImageEmbedder { private byte[] downloadWithRetry(String url) throws IOException, TimeoutException { validateHttpsUrl(url); + List candidates = downloadCandidates(url); IOException last = null; TimeoutException lastTimeout = null; long waitSeconds = downloadTimeoutSeconds * 2L; for (int attempt = 0; attempt <= DOWNLOAD_MAX_RETRY; attempt++) { - Future future = downloadPool.submit(() -> doFetch(url)); + String attemptUrl = candidates.get(Math.min(attempt, candidates.size() - 1)); + Future future = downloadPool.submit(() -> doFetch(attemptUrl)); 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) { Throwable cause = ee.getCause(); if (cause instanceof DownloadOversizeException doe) { @@ -360,7 +368,7 @@ public class SimilarAsinImageEmbedder { if (cause instanceof IOException io) { last = io; lastTimeout = null; - logRetry(url, attempt, io); + logRetry(attemptUrl, attempt, io); continue; } if (cause instanceof RuntimeException re) { @@ -371,11 +379,12 @@ public class SimilarAsinImageEmbedder { future.cancel(true); TimeoutException wrapped = new TimeoutException("image download timeout after attempt " + (attempt + 1) + "/" + (DOWNLOAD_MAX_RETRY + 1) - + ", waitSeconds=" + waitSeconds); + + ", waitSeconds=" + waitSeconds + + ", url=" + attemptUrl); wrapped.initCause(te); lastTimeout = wrapped; last = null; - logRetry(url, attempt, wrapped); + logRetry(attemptUrl, attempt, wrapped); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); 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"); } + static List downloadCandidates(String url) { + List 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 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) { if (attempt < DOWNLOAD_MAX_RETRY) { log.debug("[similar-asin][image] download-retry url={} nextAttempt={}/{} err={}", diff --git a/backend-java/src/main/resources/db/V60__split_version_admin_menu.sql b/backend-java/src/main/resources/db/V60__split_version_admin_menu.sql new file mode 100644 index 0000000..275f1df --- /dev/null +++ b/backend-java/src/main/resources/db/V60__split_version_admin_menu.sql @@ -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'; diff --git a/backend-java/src/test/java/com/nanri/aiimage/modules/productrisk/service/ProductRiskExcelAssemblyServiceTest.java b/backend-java/src/test/java/com/nanri/aiimage/modules/productrisk/service/ProductRiskExcelAssemblyServiceTest.java new file mode 100644 index 0000000..05cbd42 --- /dev/null +++ b/backend-java/src/test/java/com/nanri/aiimage/modules/productrisk/service/ProductRiskExcelAssemblyServiceTest.java @@ -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; + } +} diff --git a/backend-java/src/test/java/com/nanri/aiimage/modules/similarasin/util/SimilarAsinImageEmbedderTest.java b/backend-java/src/test/java/com/nanri/aiimage/modules/similarasin/util/SimilarAsinImageEmbedderTest.java index 4af7244..091d9d2 100644 --- a/backend-java/src/test/java/com/nanri/aiimage/modules/similarasin/util/SimilarAsinImageEmbedderTest.java +++ b/backend-java/src/test/java/com/nanri/aiimage/modules/similarasin/util/SimilarAsinImageEmbedderTest.java @@ -177,6 +177,25 @@ class SimilarAsinImageEmbedderTest { assertNotNull(req.header("Accept-Language")); } + @Test + void amazonImageDownloadCandidatesTrySmallerVariantsWithinRetryBudget() { + List 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 void buildImageRequestUsesDownloadHeadersForCozeSignedImages() { var req = SimilarAsinImageEmbedder.buildImageRequest( diff --git a/backend/blueprints/admin_api.py b/backend/blueprints/admin_api.py index af1c25c..2bf11c3 100644 --- a/backend/blueprints/admin_api.py +++ b/backend/blueprints/admin_api.py @@ -111,6 +111,11 @@ ADMIN_MENU_ACCESS_CONFIG.update({ 'route_path': 'version', 'error': '无权访问版本管理模块', }, + 'digital-human-version': { + 'column_key': 'digital_human_version', + 'route_path': 'digital-human-version', + 'error': '无权访问数字人版本管理模块', + }, }) @@ -1436,7 +1441,7 @@ def list_versions(): @admin_required def list_digital_human_versions(): """代理:查询数字人版本列表""" - _, _, denied = _ensure_admin_menu_access('version') + _, _, denied = _ensure_admin_menu_access('digital-human-version') if denied: return denied page = request.args.get('page', '1') @@ -1460,6 +1465,9 @@ def list_digital_human_versions(): @admin_required 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( 'GET', '/api/digital-human/versions/latest', @@ -1474,7 +1482,7 @@ def get_latest_digital_human_version(): @admin_required def upload_digital_human_version(): """代理:上传数字人版本""" - _, _, denied = _ensure_admin_menu_access('version') + _, _, denied = _ensure_admin_menu_access('digital-human-version') if denied: return denied @@ -1511,7 +1519,7 @@ def upload_digital_human_version(): @admin_required def release_digital_human_version(version): """代理:发布数字人版本""" - _, _, denied = _ensure_admin_menu_access('version') + _, _, denied = _ensure_admin_menu_access('digital-human-version') if denied: return denied result, error_response, status_code = _proxy_backend_java( @@ -1528,7 +1536,7 @@ def release_digital_human_version(version): @admin_required def set_latest_digital_human_version(version): """代理:设为最新数字人版本""" - _, _, denied = _ensure_admin_menu_access('version') + _, _, denied = _ensure_admin_menu_access('digital-human-version') if denied: return denied result, error_response, status_code = _proxy_backend_java( @@ -1545,7 +1553,7 @@ def set_latest_digital_human_version(version): @admin_required def delete_digital_human_version(version): """代理:删除数字人版本""" - _, _, denied = _ensure_admin_menu_access('version') + _, _, denied = _ensure_admin_menu_access('digital-human-version') if denied: return denied result, error_response, status_code = _proxy_backend_java( diff --git a/backend/config.py b/backend/config.py index 32e80cc..8e1beb2 100644 --- a/backend/config.py +++ b/backend/config.py @@ -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( "java_api_base", "BACKEND_JAVA_BASE_URL", - # default="http://api.aishufu.top:18080/", - default="http://127.0.0.1:18080/", + default="http://api.aishufu.top:18080/", + # default="http://127.0.0.1:18080/", ) backend_java_base_url = backend_java_base_url.rstrip("/") os.environ["OSS_ACCESS_KEY_ID"] = accessKeyId diff --git a/backend/static/admin.js b/backend/static/admin.js index 0e85bf2..5a6b3a5 100644 --- a/backend/static/admin.js +++ b/backend/static/admin.js @@ -108,7 +108,8 @@ 'query-asin': 'panel-query-asin', 'product-categories': 'panel-product-categories', 'history': 'panel-history', - 'version': 'panel-version' + 'version': 'panel-version', + 'digital-human-version': 'panel-digital-human-version' }; function runTabLoader(tabName) { if (tabName === 'users') { loadUsers(1); loadColumnsForPermission(); } @@ -121,7 +122,8 @@ else if (tabName === 'query-asin') loadQueryAsin(1); else if (tabName === 'product-categories') loadProductCategories(); 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() { 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 = '加载失败: ' + (res.error || '') + ''; + return; + } + var items = res.items || []; + if (!items.length) { + tbody.innerHTML = '暂无版本记录'; + return; + } + tbody.innerHTML = items.map(function (v) { + var url = v.file_url || ''; + return '' + (v.version || '') + '' + url + '' + (v.created_at || '') + '下载'; + }).join(''); + }) + .catch(function () { + document.getElementById('versionListBody').innerHTML = '请求失败'; + }); + } + + function loadDigitalHumanVersions(page) { page = page || 1; fetch('/api/admin/digital-human-versions?page=' + page + '&pageSize=20') .then(function (r) { return r.json(); }) .then(function (res) { - var tbody = document.getElementById('versionListBody'); + var tbody = document.getElementById('digitalHumanVersionListBody'); if (!res.success) { tbody.innerHTML = '加载失败: ' + (res.message || res.error || '') + ''; return; @@ -3585,12 +3613,12 @@ } // 分页 - var pagination = document.getElementById('versionPagination'); + var pagination = document.getElementById('digitalHumanVersionPagination'); if (pages > 1) { var html = ''; for (var i = 1; i <= pages; i++) { var active = i === current ? ' active' : ''; - html += '' + i + ''; + html += '' + i + ''; } pagination.innerHTML = html; } else { @@ -3598,14 +3626,13 @@ } }) .catch(function (err) { - document.getElementById('versionListBody').innerHTML = '请求失败: ' + (err.message || '') + ''; + document.getElementById('digitalHumanVersionListBody').innerHTML = '请求失败: ' + (err.message || '') + ''; }); } + window.loadDigitalHumanVersions = loadDigitalHumanVersions; document.getElementById('btnUploadVersion').onclick = function () { 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 msgEl = document.getElementById('msgVersion'); msgEl.textContent = ''; @@ -3628,6 +3655,62 @@ 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(); formData.append('file', file); formData.append('version', version); @@ -3646,11 +3729,11 @@ if (res.success && res.data) { msgEl.textContent = '上传成功!版本:' + res.data.version + '(状态:草稿,请在列表中点击"发布"按钮)'; msgEl.classList.add('ok'); - document.getElementById('versionNumber').value = ''; - document.getElementById('versionMinClientVersion').value = ''; - document.getElementById('versionChangelog').value = ''; + document.getElementById('digitalHumanVersionNumber').value = ''; + document.getElementById('digitalHumanMinClientVersion').value = ''; + document.getElementById('digitalHumanVersionChangelog').value = ''; fileInput.value = ''; - loadVersions(); + loadDigitalHumanVersions(); } else { msgEl.textContent = res.message || res.error || '上传失败'; msgEl.classList.add('err'); @@ -3671,7 +3754,7 @@ .then(function (res) { if (res.success) { alert('发布成功!'); - loadVersions(); + loadDigitalHumanVersions(); } else { alert('发布失败:' + (res.message || res.error || '')); } @@ -3690,7 +3773,7 @@ .then(function (res) { if (res.success) { alert('设置成功!版本 ' + version + ' 已标记为最新。'); - loadVersions(); + loadDigitalHumanVersions(); } else { alert('设置失败:' + (res.message || res.error || '')); } @@ -3709,7 +3792,7 @@ .then(function (res) { if (res.success) { alert('删除成功!'); - loadVersions(); + loadDigitalHumanVersions(); } else { alert('删除失败:' + (res.message || res.error || '')); } diff --git a/backend/web_source/admin.html b/backend/web_source/admin.html index c7ca3df..50f788e 100644 --- a/backend/web_source/admin.html +++ b/backend/web_source/admin.html @@ -793,7 +793,8 @@
查询 ASIN
商品类目
查看生成记录
-
版本管理
+
软件版本管理
+
数字人版本管理
@@ -916,8 +917,46 @@ - +
+
+

上传新版本

+

+ 上传软件 ZIP 包,系统会自动计算 MD5 并存储到 OSS。 +

+
+
+ + +
+
+
+ + +

仅支持 .zip 格式

+
+ +

+
+
+

版本列表

+ + + + + + + + + + +
版本号下载链接创建时间操作
+ +
+
+ + +

上传新版本

@@ -926,24 +965,23 @@

- +
- +
- +
- -

仅支持 .zip 格式,文件名:ShuFuDigitalHuman.zip

+
- -

+ +

版本列表

@@ -960,9 +998,9 @@ 操作 - + - +