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:
@@ -0,0 +1,78 @@
|
||||
/** 教程包接口适配(教程管理页):列表 / 直传预签名 / 确认 / 删除。 */
|
||||
import axios from 'axios'
|
||||
import { http, unwrap } from '@/api/http'
|
||||
import { parseTutorialPackageList, parseTutorialPackageUpload } from './tutorial-model.ts'
|
||||
import type { TutorialPackageItem, TutorialPackageList } from './tutorial-dto.ts'
|
||||
|
||||
export const TUTORIALS_ENDPOINT = '/api/admin/tutorials'
|
||||
export const TUTORIAL_UPLOAD_ENDPOINT = '/api/admin/tutorial'
|
||||
export const TUTORIAL_PRESIGN_ENDPOINT = `${TUTORIAL_UPLOAD_ENDPOINT}/presign`
|
||||
export const TUTORIAL_CONFIRM_ENDPOINT = `${TUTORIAL_UPLOAD_ENDPOINT}/confirm`
|
||||
export const TUTORIAL_DELETE_ENDPOINT = `${TUTORIAL_UPLOAD_ENDPOINT}/delete`
|
||||
|
||||
/** MinIO 直传专用实例:不带 cookie、不挂 401 跳登录拦截器(预签名过期/签名失败不能误判会话过期);大 zip 给足超时。 */
|
||||
const directPut = axios.create({ timeout: 600_000, withCredentials: false })
|
||||
|
||||
export async function fetchTutorialPackages(): Promise<TutorialPackageList> {
|
||||
const { data } = await http.get<unknown>(TUTORIALS_ENDPOINT)
|
||||
return parseTutorialPackageList(data)
|
||||
}
|
||||
|
||||
export interface TutorialUploadTarget {
|
||||
objectKey: string
|
||||
uploadUrl: string
|
||||
fileUrl: string
|
||||
}
|
||||
|
||||
/** 第一步:向后端申请直传 PUT 预签名地址(登录会话签发,3 分钟有效)。 */
|
||||
export async function requestTutorialPresign(fileName: string): Promise<TutorialUploadTarget> {
|
||||
const { data } = await http.post<unknown>(TUTORIAL_PRESIGN_ENDPOINT, null, { params: { file_name: fileName } })
|
||||
const core = (unwrap(data) ?? {}) as Record<string, unknown>
|
||||
const uploadUrl = typeof core.upload_url === 'string' ? core.upload_url : ''
|
||||
const objectKey = typeof core.object_key === 'string' ? core.object_key : ''
|
||||
if (!uploadUrl || !objectKey) {
|
||||
throw new Error('后端未返回直传地址,请重试')
|
||||
}
|
||||
return {
|
||||
objectKey,
|
||||
uploadUrl,
|
||||
fileUrl: typeof core.file_url === 'string' ? core.file_url : '',
|
||||
}
|
||||
}
|
||||
|
||||
/** 第三步:直传完成后通知后端校验对象并写入记录(data.item 为新记录行)。 */
|
||||
export async function confirmTutorialPackage(objectKey: string, fileName: string): Promise<TutorialPackageItem | null> {
|
||||
const { data } = await http.post<unknown>(TUTORIAL_CONFIRM_ENDPOINT, null, {
|
||||
params: { object_key: objectKey, file_name: fileName },
|
||||
})
|
||||
return parseTutorialPackageUpload(data)
|
||||
}
|
||||
|
||||
/** 按 id 批量删除教程包(后端会顺带清理无引用对象的 MinIO 包体);返回实际删除行数。 */
|
||||
export async function deleteTutorialPackages(ids: number[]): Promise<number> {
|
||||
if (!ids.length) return 0
|
||||
const { data } = await http.post<unknown>(TUTORIAL_DELETE_ENDPOINT, ids)
|
||||
const core = (unwrap(data) ?? {}) as Record<string, unknown>
|
||||
return typeof core.deleted === 'number' ? core.deleted : ids.length
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传教程包(浏览器直传):presign 申请 → 直接 PUT 到 MinIO client 桶(不走 Java 中转)
|
||||
* → confirm 校验落库。onProgress 回传 0-100 上传进度。
|
||||
*/
|
||||
export async function uploadTutorialPackage(
|
||||
file: Blob,
|
||||
fileName: string,
|
||||
onProgress?: (percent: number) => void,
|
||||
): Promise<TutorialPackageItem | null> {
|
||||
const target = await requestTutorialPresign(fileName)
|
||||
await directPut.put(target.uploadUrl, file, {
|
||||
headers: { 'Content-Type': 'application/octet-stream' },
|
||||
onUploadProgress: (event) => {
|
||||
if (!onProgress) return
|
||||
const total = event.total || 0
|
||||
onProgress(total > 0 ? Math.min(Math.round((event.loaded / total) * 100), 100) : 0)
|
||||
},
|
||||
})
|
||||
return confirmTutorialPackage(target.objectKey, fileName)
|
||||
}
|
||||
Reference in New Issue
Block a user