feat(教程管理): 后台可上传教程包,工具台按最新上传下载

- admin-vue 新增「记录与版本 → 教程管理」页(menuKey admin_tutorial):
  zip 浏览器直传 MinIO(presign → PUT 带进度 → confirm 落库)+ 列表/下载/删除,
  最新一条标「当前生效」;补操作指引与 align-tutorial 守卫测试。
- frontend-vue 工具台「立即下载教程」进页面取 /api/tutorial/latest(最新上传优先),
  接口异常或无记录回退历史固定直链,按钮不失效。
- Java 侧 tutorial 模块与 V119 迁移已在 dc6e8924 提交并上线(2026-09-13)。

vue-tsc + 两端单测(工具台 695 / 后台 1617)通过;生产已实测
presign→PUT→confirm→latest 切换→删除回落全链路。
This commit is contained in:
2026-09-13 23:48:24 +08:00
parent 14a723ab1c
commit 9ffd68bae5
13 changed files with 722 additions and 7 deletions
@@ -126,7 +126,7 @@ import { useRouter } from 'vue-router'
import AmazonTopBar from '@/pages/amazon/components/AmazonTopBar.vue'
import AmazonFooterBar from '@/pages/amazon/components/AmazonFooterBar.vue'
import ToolIcon from '@/pages/amazon/components/ToolIcon.vue'
import { TUTORIAL_DOWNLOAD_URL } from '@/pages/amazon/download-btn'
import { TUTORIAL_DOWNLOAD_URL, resolveTutorialDownloadUrl } from '@/pages/amazon/download-btn'
import { filterGroupsByPermission, filterWorkflowByPermission, TOOL_GROUPS, TOOL_MAP } from '@/pages/amazon/tool-catalog'
import type { ToolGroup, ToolInfo, VisibleGroup, WorkflowStep } from '@/pages/amazon/tool-catalog'
import { resolvePageHref } from '@/shared/page-prefix'
@@ -217,13 +217,20 @@ function toast(msg: string, type: 'normal' | 'error' = 'normal') {
}, 2500)
}
/** 教程包直链:进页面时解析一次(后台「教程管理」最新上传优先),失败自动回退固定直链 */
const tutorialUrl = ref(TUTORIAL_DOWNLOAD_URL)
async function refreshTutorialUrl() {
tutorialUrl.value = await resolveTutorialDownloadUrl()
}
async function onDownloadClick() {
// 教程客户端压缩包取自 MinIO 公开直链(client/tutorial/见 download-btn.ts
// 教程客户端压缩包:最新上传的包优先(/api/tutorial/latest),兜底走固定直链(见 download-btn.ts
const api = getPywebviewApi()
if (api?.open_external_url) {
// 桌面端:交系统默认浏览器下载,可看到下载进度
try {
const res = await api.open_external_url(TUTORIAL_DOWNLOAD_URL)
const res = await api.open_external_url(tutorialUrl.value)
if (!res?.success) toast(`下载失败:${res?.error ?? '请重试'}`, 'error')
} catch (_error) {
toast('下载失败,请重试', 'error')
@@ -231,7 +238,7 @@ async function onDownloadClick() {
return
}
// 网页端:新窗口打开直链下载
window.open(TUTORIAL_DOWNLOAD_URL, '_blank', 'noopener')
window.open(tutorialUrl.value, '_blank', 'noopener')
}
function onCatChange(key: 'front' | 'ops' | 'logi') {
@@ -246,6 +253,8 @@ function parseHashGroup() {
}
onMounted(async () => {
// 教程包直链异步解析(不阻塞页面渲染;失败自动回退固定直链)
void refreshTutorialUrl()
try {
allowedKeys.value = await getCurrentUserAppColumnKeys()
} catch (_error) {
+16 -3
View File
@@ -1,7 +1,20 @@
/**
* 工具台首页「立即下载教程」:数富AI 教学客户端压缩包公开直链。
* 工具台首页「立即下载教程」:优先取后台「教程管理」最新上传的包(GET /api/tutorial/latest
* 见 shared/api/tutorial.ts);接口异常或无记录时回退到历史固定直链。
*
* 对象位于 MinIO 公开 client 桶(host A minio 容器,openresty 反代 oss.aishufu.top),
* 由 scripts/upload_tutorial_zip.py 上传(同 key 覆盖更新)。
* 固定直链对象位于 MinIO 公开 client 桶(host A minio 容器,openresty 反代 oss.aishufu.top),
* 由 scripts/upload_tutorial_zip.py 上传(同 key 覆盖更新),保留作为兜底
*/
import { fetchLatestTutorialPackage } from '@/shared/api/tutorial'
export const TUTORIAL_DOWNLOAD_URL = 'https://oss.aishufu.top/client/tutorial/数富AI-教学客户端.zip'
/** 解析当前生效的教程包直链:最新上传优先;接口异常/无记录时静默回退固定直链,保证按钮始终可下载。 */
export async function resolveTutorialDownloadUrl(): Promise<string> {
try {
const latest = await fetchLatestTutorialPackage()
return latest?.fileUrl || TUTORIAL_DOWNLOAD_URL
} catch {
return TUTORIAL_DOWNLOAD_URL
}
}
+3
View File
@@ -262,6 +262,9 @@ export const API_ENDPOINTS = {
delete: '/api/digital-human/versions/{version}',
downloadUrl: '/api/digital-human/versions/{version}/download-url',
},
tutorial: {
latest: '/api/tutorial/latest',
},
} as const
export type ApiEndpointGroup = {
+1
View File
@@ -0,0 +1 @@
export * from './types/modules/tutorial'
@@ -0,0 +1,31 @@
import { get, type JavaApiResponse, unwrapJavaResponse } from '../../http.ts'
import { buildJavaUrl } from '../../url.ts'
import { API_ENDPOINTS } from '../../endpoints.ts'
/** 工具台首页「立即下载教程」的最新教程包(后台「教程管理」上传,以最新上传为主)。 */
export interface LatestTutorialPackage {
fileName: string
fileUrl: string
fileSize: number
createdAt: string
}
/** 读取最新教程包;后端无记录时返回 null(调用方回退到历史固定直链)。 */
export async function fetchLatestTutorialPackage(): Promise<LatestTutorialPackage | null> {
const payload = await unwrapJavaResponse(
get<JavaApiResponse<Record<string, unknown>>>(buildJavaUrl(API_ENDPOINTS.tutorial.latest)),
)
if (!payload || typeof payload !== 'object') {
return null
}
const fileUrl = typeof payload.file_url === 'string' ? payload.file_url.trim() : ''
if (!fileUrl) {
return null
}
return {
fileName: typeof payload.file_name === 'string' ? payload.file_name.trim() : '',
fileUrl,
fileSize: typeof payload.file_size === 'number' ? payload.file_size : 0,
createdAt: typeof payload.created_at === 'string' ? payload.created_at : '',
}
}
+3
View File
@@ -369,6 +369,9 @@ test('test_endpoints_frozen_snapshot', () => {
"setLatest": "/api/digital-human/versions/{version}/set-latest",
"delete": "/api/digital-human/versions/{version}",
"downloadUrl": "/api/digital-human/versions/{version}/download-url"
},
"tutorial": {
"latest": "/api/tutorial/latest"
}
}, '常量表快照不应漂移')
})