feat(upload): parse=upload+save+metadata, register=JSON only — no double upload
Previously the flow uploaded the 53MB file twice:
1. POST /parse → parse metadata (file discarded)
2. POST /upload → parse again + save (file sent again)
New flow — file sent exactly once:
1. POST /parse → upload file, save to disk, parse metadata
returns {versionName, versionCode, minSdkVersion, storageKey, fileSize, fileSha256}
2. POST /register → JSON only (no file), creates DB record using storageKey
Frontend:
- handleFileChange: async, immediately uploads to /parse with progress bar (0-100%)
- handleSubmit: calls /register with storageKey + form metadata (instant)
- Upload modal: real-time progress bar, "confirm" button disabled until parse complete
- Console logs at every step for debugging
Backend:
- POST /parse: saves file after parsing, returns storageKey in response
- POST /register: new endpoint, accepts JSON + storageKey, creates version record
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
5ce4dd2442
commit
7c8b79161a
|
|
@ -147,16 +147,82 @@ export class AdminVersionController {
|
||||||
@Post('parse')
|
@Post('parse')
|
||||||
@UseInterceptors(FileInterceptor('file', { limits: { fileSize: 500 * 1024 * 1024 } }))
|
@UseInterceptors(FileInterceptor('file', { limits: { fileSize: 500 * 1024 * 1024 } }))
|
||||||
@ApiConsumes('multipart/form-data')
|
@ApiConsumes('multipart/form-data')
|
||||||
@ApiOperation({ summary: 'Parse APK/IPA without saving (preview metadata)' })
|
@ApiOperation({ summary: 'Parse APK/IPA — saves file, returns metadata + storageKey for /register' })
|
||||||
async parsePackage(@UploadedFile() file: Express.Multer.File) {
|
async parsePackage(@UploadedFile() file: Express.Multer.File) {
|
||||||
const t0 = Date.now();
|
const t0 = Date.now();
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return { code: 400, message: 'No file uploaded — check Content-Type boundary' };
|
return { code: 400, message: 'No file uploaded — check Content-Type boundary' };
|
||||||
}
|
}
|
||||||
this.logger.log(`[parse] received: ${file.originalname} size=${(file.size / 1024 / 1024).toFixed(1)}MB`);
|
this.logger.log(`[parse] received: ${file.originalname} size=${(file.size / 1024 / 1024).toFixed(1)}MB`);
|
||||||
|
|
||||||
|
const t1 = Date.now();
|
||||||
const info = await this.packageParser.parse(file.buffer, file.originalname);
|
const info = await this.packageParser.parse(file.buffer, file.originalname);
|
||||||
this.logger.log(`[parse] done in ${Date.now() - t0}ms → pkg=${info.packageName} ver=${info.versionName}`);
|
this.logger.log(`[parse] metadata done in ${Date.now() - t1}ms → pkg=${info.packageName} ver=${info.versionName}`);
|
||||||
return { code: 0, data: info };
|
|
||||||
|
// Save file so /register can reference it without re-uploading
|
||||||
|
const platform = file.originalname.toLowerCase().endsWith('.ipa') ? 'IOS' : 'ANDROID';
|
||||||
|
const t2 = Date.now();
|
||||||
|
const storageKey = this.fileStorage.generateObjectName(file.originalname, platform, info.versionName || 'unknown');
|
||||||
|
const saved = await this.fileStorage.saveFile(file.buffer, storageKey);
|
||||||
|
this.logger.log(`[parse] file saved in ${Date.now() - t2}ms → ${storageKey} (total ${Date.now() - t0}ms)`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: 0,
|
||||||
|
data: {
|
||||||
|
...info,
|
||||||
|
storageKey,
|
||||||
|
fileSize: String(saved.size),
|
||||||
|
fileSha256: saved.sha256,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('register')
|
||||||
|
@ApiOperation({ summary: 'Register version using storageKey from /parse — no file upload needed' })
|
||||||
|
async registerVersion(
|
||||||
|
@Body() body: {
|
||||||
|
storageKey: string;
|
||||||
|
fileSize: string;
|
||||||
|
fileSha256: string;
|
||||||
|
appType?: string;
|
||||||
|
platform?: string;
|
||||||
|
versionCode?: number;
|
||||||
|
versionName?: string;
|
||||||
|
buildNumber?: string;
|
||||||
|
changelog?: string;
|
||||||
|
isForceUpdate?: boolean;
|
||||||
|
minOsVersion?: string;
|
||||||
|
releaseDate?: string;
|
||||||
|
},
|
||||||
|
@Req() req: any,
|
||||||
|
) {
|
||||||
|
if (!body.storageKey) return { code: 400, message: 'storageKey is required' };
|
||||||
|
|
||||||
|
const platform = (body.platform || 'ANDROID').toUpperCase() as Platform;
|
||||||
|
const appType = (body.appType || 'GENEX_MOBILE').toUpperCase() as AppType;
|
||||||
|
const versionCode = body.versionCode || 0;
|
||||||
|
|
||||||
|
this.logger.log(`[register] storageKey=${body.storageKey} ver=${body.versionName} build=${body.buildNumber}`);
|
||||||
|
|
||||||
|
const version = await this.versionService.createVersion({
|
||||||
|
appType,
|
||||||
|
platform,
|
||||||
|
versionCode,
|
||||||
|
versionName: body.versionName || '0.0.0',
|
||||||
|
buildNumber: body.buildNumber || String(versionCode),
|
||||||
|
storageKey: body.storageKey,
|
||||||
|
downloadUrl: `/api/v1/app/version/download/`,
|
||||||
|
fileSize: body.fileSize || '0',
|
||||||
|
fileSha256: body.fileSha256 || '',
|
||||||
|
changelog: body.changelog || '',
|
||||||
|
isForceUpdate: body.isForceUpdate === true,
|
||||||
|
minOsVersion: body.minOsVersion,
|
||||||
|
releaseDate: body.releaseDate ? new Date(body.releaseDate) : undefined,
|
||||||
|
createdBy: req.user?.sub,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.log(`[register] created version id=${version.id}`);
|
||||||
|
return { code: 0, data: version };
|
||||||
}
|
}
|
||||||
|
|
||||||
@Put(':id')
|
@Put(':id')
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,24 @@ export interface ParsedPackageInfo {
|
||||||
versionCode?: number;
|
versionCode?: number;
|
||||||
versionName?: string;
|
versionName?: string;
|
||||||
minSdkVersion?: string;
|
minSdkVersion?: string;
|
||||||
|
// Returned by /parse — file already saved server-side, use for /register
|
||||||
|
storageKey?: string;
|
||||||
|
fileSize?: string;
|
||||||
|
fileSha256?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegisterVersionInput {
|
||||||
|
storageKey: string;
|
||||||
|
fileSize: string;
|
||||||
|
fileSha256: string;
|
||||||
|
appType: AppType;
|
||||||
|
platform: AppPlatform;
|
||||||
|
versionCode?: number;
|
||||||
|
versionName?: string;
|
||||||
|
buildNumber?: string;
|
||||||
|
changelog?: string;
|
||||||
|
minOsVersion?: string;
|
||||||
|
isForceUpdate: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UploadVersionInput {
|
export interface UploadVersionInput {
|
||||||
|
|
@ -26,7 +44,8 @@ export interface UpdateVersionInput {
|
||||||
|
|
||||||
export interface IVersionRepository {
|
export interface IVersionRepository {
|
||||||
list(appType: AppType, platform?: AppPlatform, includeDisabled?: boolean): Promise<AppVersion[]>;
|
list(appType: AppType, platform?: AppPlatform, includeDisabled?: boolean): Promise<AppVersion[]>;
|
||||||
parse(file: File): Promise<ParsedPackageInfo>;
|
parse(file: File, onProgress?: (pct: number) => void): Promise<ParsedPackageInfo>;
|
||||||
|
register(input: RegisterVersionInput): Promise<AppVersion>;
|
||||||
upload(input: UploadVersionInput): Promise<AppVersion>;
|
upload(input: UploadVersionInput): Promise<AppVersion>;
|
||||||
update(id: string, input: UpdateVersionInput): Promise<AppVersion>;
|
update(id: string, input: UpdateVersionInput): Promise<AppVersion>;
|
||||||
toggle(id: string, isEnabled: boolean): Promise<void>;
|
toggle(id: string, isEnabled: boolean): Promise<void>;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import type {
|
import type {
|
||||||
IVersionRepository,
|
IVersionRepository,
|
||||||
ParsedPackageInfo,
|
ParsedPackageInfo,
|
||||||
|
RegisterVersionInput,
|
||||||
UploadVersionInput,
|
UploadVersionInput,
|
||||||
UpdateVersionInput,
|
UpdateVersionInput,
|
||||||
} from '@/domain/repositories/version.repository.interface';
|
} from '@/domain/repositories/version.repository.interface';
|
||||||
|
|
@ -14,12 +15,40 @@ class VersionRepository implements IVersionRepository {
|
||||||
return httpClient.get<AppVersion[]>('/api/v1/admin/versions', { params });
|
return httpClient.get<AppVersion[]>('/api/v1/admin/versions', { params });
|
||||||
}
|
}
|
||||||
|
|
||||||
async parse(file: File): Promise<ParsedPackageInfo> {
|
// Parse = upload file once + get metadata + storageKey (server keeps the file)
|
||||||
|
async parse(file: File, onProgress?: (pct: number) => void): Promise<ParsedPackageInfo> {
|
||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
fd.append('file', file);
|
fd.append('file', file);
|
||||||
return httpClient.post<ParsedPackageInfo>('/api/v1/admin/versions/parse', fd, { timeout: 300000 });
|
console.log(`[VersionRepo.parse] ${file.name} (${(file.size / 1024 / 1024).toFixed(1)}MB) → POST /api/v1/admin/versions/parse`);
|
||||||
|
return httpClient.post<ParsedPackageInfo>('/api/v1/admin/versions/parse', fd, {
|
||||||
|
timeout: 600000,
|
||||||
|
onUploadProgress: (evt) => {
|
||||||
|
const pct = evt.total ? Math.round((evt.loaded / evt.total) * 100) : 0;
|
||||||
|
console.log(`[VersionRepo.parse] upload ${pct}% (${(evt.loaded / 1024 / 1024).toFixed(1)}MB / ${((evt.total ?? 0) / 1024 / 1024).toFixed(1)}MB)`);
|
||||||
|
onProgress?.(pct);
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Register = send JSON only (no file), uses storageKey from parse step
|
||||||
|
async register(input: RegisterVersionInput): Promise<AppVersion> {
|
||||||
|
console.log(`[VersionRepo.register] storageKey=${input.storageKey} ver=${input.versionName}`);
|
||||||
|
return httpClient.post<AppVersion>('/api/v1/admin/versions/register', {
|
||||||
|
storageKey: input.storageKey,
|
||||||
|
fileSize: input.fileSize,
|
||||||
|
fileSha256: input.fileSha256,
|
||||||
|
appType: input.appType,
|
||||||
|
platform: input.platform,
|
||||||
|
versionCode: input.versionCode,
|
||||||
|
versionName: input.versionName,
|
||||||
|
buildNumber: input.buildNumber,
|
||||||
|
changelog: input.changelog,
|
||||||
|
minOsVersion: input.minOsVersion,
|
||||||
|
isForceUpdate: input.isForceUpdate,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy: upload = parse + save in one request (kept for compatibility)
|
||||||
async upload(input: UploadVersionInput): Promise<AppVersion> {
|
async upload(input: UploadVersionInput): Promise<AppVersion> {
|
||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
fd.append('file', input.file);
|
fd.append('file', input.file);
|
||||||
|
|
@ -30,7 +59,7 @@ class VersionRepository implements IVersionRepository {
|
||||||
fd.append('changelog', input.changelog || '');
|
fd.append('changelog', input.changelog || '');
|
||||||
fd.append('minOsVersion', input.minOsVersion || '');
|
fd.append('minOsVersion', input.minOsVersion || '');
|
||||||
fd.append('isForceUpdate', String(input.isForceUpdate));
|
fd.append('isForceUpdate', String(input.isForceUpdate));
|
||||||
return httpClient.post<AppVersion>('/api/v1/admin/versions/upload', fd, { timeout: 300000 });
|
return httpClient.post<AppVersion>('/api/v1/admin/versions/upload', fd, { timeout: 600000 });
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(id: string, input: UpdateVersionInput): Promise<AppVersion> {
|
async update(id: string, input: UpdateVersionInput): Promise<AppVersion> {
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,16 @@ interface UploadFormState {
|
||||||
changelog: string;
|
changelog: string;
|
||||||
minOsVersion: string;
|
minOsVersion: string;
|
||||||
isForceUpdate: boolean;
|
isForceUpdate: boolean;
|
||||||
|
// Parse phase (file upload + metadata extraction)
|
||||||
isParsing: boolean;
|
isParsing: boolean;
|
||||||
|
parseProgress: number; // 0-100 during file upload to /parse
|
||||||
|
parseDone: boolean; // true = file on server, ready to register
|
||||||
parseWarning: string;
|
parseWarning: string;
|
||||||
|
storageKey: string;
|
||||||
|
fileSize: string;
|
||||||
|
fileSha256: string;
|
||||||
|
versionCode: number;
|
||||||
|
// Register phase (JSON only, instant)
|
||||||
isUploading: boolean;
|
isUploading: boolean;
|
||||||
uploadError: string;
|
uploadError: string;
|
||||||
}
|
}
|
||||||
|
|
@ -32,7 +40,13 @@ interface UploadStoreActions {
|
||||||
setMinOsVersion: (v: string) => void;
|
setMinOsVersion: (v: string) => void;
|
||||||
setIsForceUpdate: (v: boolean) => void;
|
setIsForceUpdate: (v: boolean) => void;
|
||||||
setIsParsing: (v: boolean) => void;
|
setIsParsing: (v: boolean) => void;
|
||||||
|
setParseProgress: (v: number) => void;
|
||||||
|
setParseDone: (v: boolean) => void;
|
||||||
setParseWarning: (v: string) => void;
|
setParseWarning: (v: string) => void;
|
||||||
|
setStorageKey: (v: string) => void;
|
||||||
|
setFileSize: (v: string) => void;
|
||||||
|
setFileSha256: (v: string) => void;
|
||||||
|
setVersionCode: (v: number) => void;
|
||||||
setIsUploading: (v: boolean) => void;
|
setIsUploading: (v: boolean) => void;
|
||||||
setUploadError: (v: string) => void;
|
setUploadError: (v: string) => void;
|
||||||
reset: () => void;
|
reset: () => void;
|
||||||
|
|
@ -47,7 +61,13 @@ const initialState: UploadFormState = {
|
||||||
minOsVersion: '',
|
minOsVersion: '',
|
||||||
isForceUpdate: false,
|
isForceUpdate: false,
|
||||||
isParsing: false,
|
isParsing: false,
|
||||||
|
parseProgress: 0,
|
||||||
|
parseDone: false,
|
||||||
parseWarning: '',
|
parseWarning: '',
|
||||||
|
storageKey: '',
|
||||||
|
fileSize: '',
|
||||||
|
fileSha256: '',
|
||||||
|
versionCode: 0,
|
||||||
isUploading: false,
|
isUploading: false,
|
||||||
uploadError: '',
|
uploadError: '',
|
||||||
};
|
};
|
||||||
|
|
@ -62,7 +82,13 @@ export const useUploadStore = create<UploadFormState & UploadStoreActions>()((se
|
||||||
setMinOsVersion: (minOsVersion) => set({ minOsVersion }),
|
setMinOsVersion: (minOsVersion) => set({ minOsVersion }),
|
||||||
setIsForceUpdate: (isForceUpdate) => set({ isForceUpdate }),
|
setIsForceUpdate: (isForceUpdate) => set({ isForceUpdate }),
|
||||||
setIsParsing: (isParsing) => set({ isParsing }),
|
setIsParsing: (isParsing) => set({ isParsing }),
|
||||||
|
setParseProgress: (parseProgress) => set({ parseProgress }),
|
||||||
|
setParseDone: (parseDone) => set({ parseDone }),
|
||||||
setParseWarning: (parseWarning) => set({ parseWarning }),
|
setParseWarning: (parseWarning) => set({ parseWarning }),
|
||||||
|
setStorageKey: (storageKey) => set({ storageKey }),
|
||||||
|
setFileSize: (fileSize) => set({ fileSize }),
|
||||||
|
setFileSha256: (fileSha256) => set({ fileSha256 }),
|
||||||
|
setVersionCode: (versionCode) => set({ versionCode }),
|
||||||
setIsUploading: (isUploading) => set({ isUploading }),
|
setIsUploading: (isUploading) => set({ isUploading }),
|
||||||
setUploadError: (uploadError) => set({ uploadError }),
|
setUploadError: (uploadError) => set({ uploadError }),
|
||||||
reset: () => set(initialState),
|
reset: () => set(initialState),
|
||||||
|
|
|
||||||
|
|
@ -255,7 +255,8 @@ const UploadModal: React.FC<{
|
||||||
// 读 Zustand 状态
|
// 读 Zustand 状态
|
||||||
const {
|
const {
|
||||||
file, platform, versionName, buildNumber, changelog, minOsVersion, isForceUpdate,
|
file, platform, versionName, buildNumber, changelog, minOsVersion, isForceUpdate,
|
||||||
isParsing, parseWarning, isUploading, uploadError,
|
isParsing, parseProgress, parseDone, parseWarning,
|
||||||
|
isUploading, uploadError,
|
||||||
setPlatform, setVersionName, setBuildNumber, setChangelog,
|
setPlatform, setVersionName, setBuildNumber, setChangelog,
|
||||||
setMinOsVersion, setIsForceUpdate, reset,
|
setMinOsVersion, setIsForceUpdate, reset,
|
||||||
} = useUploadStore();
|
} = useUploadStore();
|
||||||
|
|
@ -272,17 +273,38 @@ const UploadModal: React.FC<{
|
||||||
|
|
||||||
<label style={labelStyle}>{t('app_version_upload_file')}</label>
|
<label style={labelStyle}>{t('app_version_upload_file')}</label>
|
||||||
<input type="file" accept=".apk,.ipa" onChange={handleFileChange}
|
<input type="file" accept=".apk,.ipa" onChange={handleFileChange}
|
||||||
|
disabled={isParsing}
|
||||||
style={{ ...inputStyle, padding: '8px 12px', height: 'auto' }} />
|
style={{ ...inputStyle, padding: '8px 12px', height: 'auto' }} />
|
||||||
{file && (
|
{file && (
|
||||||
<div style={{ color: 'var(--color-text-secondary)', marginTop: 4 }}>
|
<div style={{ color: 'var(--color-text-secondary)', marginTop: 4 }}>
|
||||||
{file.name} ({(file.size / 1024 / 1024).toFixed(1)} MB)
|
{file.name} ({(file.size / 1024 / 1024).toFixed(1)} MB)
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 上传进度条(选文件后立即开始上传到 /parse) */}
|
||||||
{isParsing && (
|
{isParsing && (
|
||||||
<div style={{ color: 'var(--color-info)', marginTop: 4 }}>
|
<div style={{ marginTop: 8 }}>
|
||||||
{t('app_version_parsing')}{file && file.size > 30 * 1024 * 1024 ? '(大文件,请耐心等待...)' : ''}
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
|
||||||
|
<span style={{ color: 'var(--color-info)', fontSize: 13 }}>正在上传并解析...</span>
|
||||||
|
<span style={{ color: 'var(--color-info)', fontSize: 13, fontWeight: 600 }}>{parseProgress}%</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ height: 8, background: 'var(--color-gray-100)', borderRadius: 4, overflow: 'hidden' }}>
|
||||||
|
<div style={{
|
||||||
|
height: '100%',
|
||||||
|
width: `${parseProgress}%`,
|
||||||
|
background: 'var(--color-primary)',
|
||||||
|
borderRadius: 4,
|
||||||
|
transition: 'width 0.3s ease',
|
||||||
|
}} />
|
||||||
|
</div>
|
||||||
|
{parseProgress === 100 && (
|
||||||
|
<div style={{ color: 'var(--color-text-tertiary)', fontSize: 12, marginTop: 4 }}>文件已到达服务器,正在解析元数据...</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{parseDone && !isParsing && (
|
||||||
|
<div style={{ color: 'var(--color-success, #0a0)', marginTop: 4, fontSize: 13 }}>✓ 上传解析完成,请确认版本信息后点击确认</div>
|
||||||
|
)}
|
||||||
{parseWarning && (
|
{parseWarning && (
|
||||||
<div style={{ color: 'var(--color-warning, #f90)', marginTop: 4 }}>{parseWarning}</div>
|
<div style={{ color: 'var(--color-warning, #f90)', marginTop: 4 }}>{parseWarning}</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -327,9 +349,7 @@ const UploadModal: React.FC<{
|
||||||
|
|
||||||
{uploadError && <div style={{ color: 'var(--color-error)', marginTop: 8 }}>{uploadError}</div>}
|
{uploadError && <div style={{ color: 'var(--color-error)', marginTop: 8 }}>{uploadError}</div>}
|
||||||
{isUploading && (
|
{isUploading && (
|
||||||
<div style={{ color: 'var(--color-info)', marginTop: 4 }}>
|
<div style={{ color: 'var(--color-info)', marginTop: 4 }}>正在注册版本...</div>
|
||||||
正在上传,大文件需要较长时间,请耐心等待...
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div style={{ display: 'flex', gap: 12, marginTop: 20, justifyContent: 'flex-end' }}>
|
<div style={{ display: 'flex', gap: 12, marginTop: 20, justifyContent: 'flex-end' }}>
|
||||||
|
|
@ -337,9 +357,9 @@ const UploadModal: React.FC<{
|
||||||
style={{ padding: '8px 20px', border: '1px solid var(--color-border)', borderRadius: 'var(--radius-full)', background: 'transparent', cursor: 'pointer' }}>
|
style={{ padding: '8px 20px', border: '1px solid var(--color-border)', borderRadius: 'var(--radius-full)', background: 'transparent', cursor: 'pointer' }}>
|
||||||
{t('cancel')}
|
{t('cancel')}
|
||||||
</button>
|
</button>
|
||||||
<button onClick={handleSubmit} disabled={isUploading || isParsing}
|
<button onClick={handleSubmit} disabled={isUploading || isParsing || !parseDone}
|
||||||
style={{ ...primaryBtn, opacity: isUploading || isParsing ? 0.6 : 1 }}>
|
style={{ ...primaryBtn, opacity: (isUploading || isParsing || !parseDone) ? 0.6 : 1 }}>
|
||||||
{isUploading ? t('app_version_uploading') : t('confirm')}
|
{isUploading ? '注册中...' : isParsing ? `上传中 ${parseProgress}%` : t('confirm')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,17 +3,22 @@
|
||||||
import type { ChangeEvent } from 'react';
|
import type { ChangeEvent } from 'react';
|
||||||
import type { AppType } from '@/domain/entities';
|
import type { AppType } from '@/domain/entities';
|
||||||
import { useUploadStore } from '@/store/zustand/upload.store';
|
import { useUploadStore } from '@/store/zustand/upload.store';
|
||||||
import { parsePackageUseCase, uploadVersionUseCase } from '@/application/use-cases/version.use-cases';
|
import { versionRepository } from '@/infrastructure/repositories/version.repository';
|
||||||
|
|
||||||
export function useUpload(appType: AppType, onSuccess: () => void) {
|
export function useUpload(appType: AppType, onSuccess: () => void) {
|
||||||
const store = useUploadStore();
|
const store = useUploadStore();
|
||||||
|
|
||||||
|
// 选文件 → 立即上传到 /parse(显示进度条),服务端保存文件 + 解析元数据
|
||||||
|
// /parse 完成后,form 自动填充,用户确认后只需发 JSON 到 /register(瞬间)
|
||||||
const handleFileChange = async (e: ChangeEvent<HTMLInputElement>) => {
|
const handleFileChange = async (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
const f = e.target.files?.[0];
|
const f = e.target.files?.[0];
|
||||||
if (!f) return;
|
if (!f) return;
|
||||||
|
|
||||||
store.setFile(f);
|
store.setFile(f);
|
||||||
|
store.setUploadError('');
|
||||||
store.setParseWarning('');
|
store.setParseWarning('');
|
||||||
|
store.setParseDone(false);
|
||||||
|
store.setParseProgress(0);
|
||||||
store.setVersionName('');
|
store.setVersionName('');
|
||||||
store.setBuildNumber('');
|
store.setBuildNumber('');
|
||||||
store.setMinOsVersion('');
|
store.setMinOsVersion('');
|
||||||
|
|
@ -21,43 +26,66 @@ export function useUpload(appType: AppType, onSuccess: () => void) {
|
||||||
if (f.name.endsWith('.apk')) store.setPlatform('ANDROID');
|
if (f.name.endsWith('.apk')) store.setPlatform('ANDROID');
|
||||||
else if (f.name.endsWith('.ipa')) store.setPlatform('IOS');
|
else if (f.name.endsWith('.ipa')) store.setPlatform('IOS');
|
||||||
|
|
||||||
// 先解析,等解析完成后才允许点击上传(与 RWADurian 一致)
|
console.log(`[useUpload] file selected: ${f.name} (${(f.size / 1024 / 1024).toFixed(1)}MB) — uploading to /parse`);
|
||||||
|
const t0 = Date.now();
|
||||||
store.setIsParsing(true);
|
store.setIsParsing(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const parsed = await parsePackageUseCase.execute(f);
|
const parsed = await versionRepository.parse(f, (pct) => {
|
||||||
|
store.setParseProgress(pct);
|
||||||
|
console.log(`[useUpload] parse upload progress: ${pct}%`);
|
||||||
|
});
|
||||||
|
|
||||||
|
const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
|
||||||
|
console.log(`[useUpload] parse done in ${elapsed}s → ver=${parsed.versionName} build=${parsed.versionCode} storageKey=${parsed.storageKey}`);
|
||||||
|
|
||||||
if (parsed.versionName) store.setVersionName(parsed.versionName);
|
if (parsed.versionName) store.setVersionName(parsed.versionName);
|
||||||
if (parsed.versionCode) store.setBuildNumber(String(parsed.versionCode));
|
if (parsed.versionCode) {
|
||||||
|
store.setVersionCode(parsed.versionCode);
|
||||||
|
store.setBuildNumber(String(parsed.versionCode));
|
||||||
|
}
|
||||||
if (parsed.minSdkVersion) store.setMinOsVersion(parsed.minSdkVersion);
|
if (parsed.minSdkVersion) store.setMinOsVersion(parsed.minSdkVersion);
|
||||||
} catch {
|
if (parsed.storageKey) store.setStorageKey(parsed.storageKey);
|
||||||
store.setParseWarning('无法自动解析安装包信息,请手动填写版本号和构建号');
|
if (parsed.fileSize) store.setFileSize(parsed.fileSize);
|
||||||
|
if (parsed.fileSha256) store.setFileSha256(parsed.fileSha256);
|
||||||
|
|
||||||
|
store.setParseDone(true);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[useUpload] parse failed:', err);
|
||||||
|
store.setParseWarning('解析失败,请重试或手动填写版本号和构建号');
|
||||||
} finally {
|
} finally {
|
||||||
store.setIsParsing(false);
|
store.setIsParsing(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 确认上传 → 只发 JSON(文件已在服务端),瞬间完成
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
const { file, platform, versionName, buildNumber, changelog, minOsVersion, isForceUpdate } =
|
const {
|
||||||
useUploadStore.getState();
|
platform, versionName, buildNumber, changelog, minOsVersion, isForceUpdate,
|
||||||
|
storageKey, fileSize, fileSha256, versionCode,
|
||||||
|
} = useUploadStore.getState();
|
||||||
|
|
||||||
if (!file) {
|
if (!storageKey) {
|
||||||
store.setUploadError('请选择 APK/IPA 文件');
|
store.setUploadError('请先选择文件(等待上传完成)');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
store.setIsUploading(true);
|
store.setIsUploading(true);
|
||||||
store.setUploadError('');
|
store.setUploadError('');
|
||||||
|
console.log(`[useUpload] registering version: storageKey=${storageKey} ver=${versionName} build=${buildNumber}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('[useUpload] Uploading:', file.name, (file.size / 1024 / 1024).toFixed(1) + 'MB');
|
const result = await versionRepository.register({
|
||||||
const result = await uploadVersionUseCase.execute({
|
storageKey, fileSize, fileSha256,
|
||||||
file, appType, platform, versionName, buildNumber,
|
appType, platform, versionCode,
|
||||||
changelog, minOsVersion, isForceUpdate,
|
versionName, buildNumber, changelog, minOsVersion, isForceUpdate,
|
||||||
});
|
});
|
||||||
console.log('[useUpload] Success:', result);
|
console.log(`[useUpload] registered! id=${result.id} ver=${result.versionName}`);
|
||||||
store.reset();
|
store.reset();
|
||||||
onSuccess();
|
onSuccess();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[useUpload] Failed:', err);
|
console.error('[useUpload] register failed:', err);
|
||||||
store.setUploadError((err as Error)?.message || 'Upload failed');
|
store.setUploadError((err as Error)?.message || '注册版本失败,请重试');
|
||||||
}
|
}
|
||||||
store.setIsUploading(false);
|
store.setIsUploading(false);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue