/** 教程包接口适配(教程管理页):列表 / 直传预签名 / 确认 / 删除。 */ 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 { const { data } = await http.get(TUTORIALS_ENDPOINT) return parseTutorialPackageList(data) } export interface TutorialUploadTarget { objectKey: string uploadUrl: string fileUrl: string version: string } /** 第一步:向后端申请直传 PUT 预签名地址(登录会话签发,3 分钟有效)。 */ export async function requestTutorialPresign(fileName: string, version = ''): Promise { const { data } = await http.post(TUTORIAL_PRESIGN_ENDPOINT, null, { params: { file_name: fileName, version } }) const core = (unwrap(data) ?? {}) as Record 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 : '', version: typeof core.version === 'string' ? core.version : version.trim(), } } /** 第三步:直传完成后通知后端校验对象并写入记录(data.item 为新记录行)。 */ export async function confirmTutorialPackage(objectKey: string, fileName: string, version = ''): Promise { const { data } = await http.post(TUTORIAL_CONFIRM_ENDPOINT, null, { params: { object_key: objectKey, file_name: fileName, version }, }) return parseTutorialPackageUpload(data) } /** 按 id 批量删除教程包(后端会顺带清理无引用对象的 MinIO 包体);返回实际删除行数。 */ export async function deleteTutorialPackages(ids: number[]): Promise { if (!ids.length) return 0 const { data } = await http.post(TUTORIAL_DELETE_ENDPOINT, ids) const core = (unwrap(data) ?? {}) as Record 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, version = '', onProgress?: (percent: number) => void, ): Promise { const target = await requestTutorialPresign(fileName, version) 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, target.version) }