import { Injectable } from '@angular/core'; import { NovaStorage, NovaFile } from 'fmode-ng/core'; import { FmodeParse, FmodeObject } from 'fmode-ng/parse'; const Parse = FmodeParse.with('nova'); @Injectable({ providedIn: 'root' }) export class ProjectFileService { /** * 上传项目文件并保存到ProjectFile表 * @param file 要上传的文件 * @param projectId 项目ID * @param fileType 文件类型 * @param spaceId 空间ID(可选) * @param stage 项目阶段(可选) * @param additionalMetadata 额外元数据(可选) * @param onProgress 上传进度回调 * @returns 上传后的NovaFile对象 */ async uploadProjectFile( file: File, projectId: string, fileType: string, spaceId?: string, stage?: string, additionalMetadata?: any, onProgress?: (progress: number) => void ): Promise { try { // 获取公司ID const cid = localStorage.getItem('company'); if (!cid) { throw new Error('公司ID未找到'); } // 初始化存储 const storage = await NovaStorage.withCid(cid); // 构建prefixKey let prefixKey = `project/${projectId}`; if (spaceId) { prefixKey += `/space/${spaceId}`; } if (stage) { prefixKey += `/stage/${stage}`; } // 上传文件 const uploadedFile: NovaFile = await storage.upload(file, { prefixKey, onProgress: (progress: { total: { percent: number } }) => { if (onProgress) { onProgress(progress.total.percent); } } }); // 保存到Attachment表 await this.saveToAttachmentTable(uploadedFile, projectId, fileType, spaceId, stage, additionalMetadata); return uploadedFile; } catch (error) { console.error('项目文件上传失败:', error); throw error; } } /** * 保存文件信息到Attachment表 */ private async saveToAttachmentTable( file: NovaFile, projectId: string, fileType: string, spaceId?: string, stage?: string, additionalMetadata?: any ): Promise { const attachment = new Parse.Object('Attachment'); // 设置基本字段 attachment.set('size', file.size); attachment.set('url', file.url); attachment.set('name', file.name); attachment.set('mime', file.type); attachment.set('md5', file.md5); attachment.set('metadata', { ...file.metadata, projectId, fileType, spaceId, stage, ...additionalMetadata }); // 设置关联关系 const cid = localStorage.getItem('company'); if (cid) { let company = new Parse.Object('Company'); company.id = cid if (company) { attachment.set('company', company.toPointer()); } } // 设置当前用户 const currentUser = Parse.User.current(); if (currentUser) { attachment.set('user', currentUser); } const savedAttachment = await attachment.save(); return savedAttachment; } /** * 保存到ProjectFile表 */ async saveToProjectFile( attachment: FmodeObject, projectId: string, fileType: string, spaceId?: string, stage?: string ): Promise { const projectFile = new Parse.Object('ProjectFile'); // 获取项目 const projectQuery = new Parse.Query("Project"); const project = await projectQuery.get(projectId); // 设置字段 projectFile.set('project', project); projectFile.set('attach', attachment); projectFile.set('fileType', fileType); projectFile.set('fileUrl', attachment.get('url')); projectFile.set('fileName', attachment.get('name')); projectFile.set('fileSize', attachment.get('size')); if (stage) { projectFile.set('stage', stage); } // ✨ 增强:完整保存元数据到 ProjectFile.data,包括审批状态等 const attachmentMetadata = attachment.get('metadata') || {}; const deliverableId = attachmentMetadata.deliverableId; const data = { spaceId, uploadedAt: new Date(), fileType, deliverableId, // ✨ 新增:提交信息跟踪字段 submittedAt: attachmentMetadata.submittedAt || new Date().toISOString(), submittedBy: attachmentMetadata.submittedBy || Parse.User.current()?.id, submittedByName: attachmentMetadata.submittedByName || Parse.User.current()?.get('name') || Parse.User.current()?.get('username'), modifiedAt: new Date().toISOString(), modifiedBy: Parse.User.current()?.id, modifiedByName: Parse.User.current()?.get('name') || Parse.User.current()?.get('username'), // AI分析结果 analysisResult: attachmentMetadata.analysisResult, aiConfidence: attachmentMetadata.analysisResult?.content?.confidence, aiSuggestedStage: attachmentMetadata.analysisResult?.suggestedStage, aiQualityScore: attachmentMetadata.analysisResult?.quality?.score, // 交付清单状态 deliveryStatus: 'submitted', // submitted, approved, rejected deliveryListId: attachmentMetadata.deliveryListId, // 交付清单ID // ✨ 保存所有元数据(包含 approvalStatus, uploadedByName, uploadedById 等) ...attachmentMetadata }; projectFile.set('data', data); // 设置上传者 const currentUser = Parse.User.current(); if (currentUser) { projectFile.set('uploadedBy', currentUser); } const savedProjectFile = await projectFile.save(); console.log('✅ ProjectFile已保存,data字段:', savedProjectFile.get('data')); return savedProjectFile; } /** * 删除项目文件 */ async deleteProjectFile(projectFileId: string): Promise { try { // 删除ProjectFile记录 const query = new Parse.Query("ProjectFile"); const projectFile = await query.get(projectFileId); // 删除Attachment记录 const attachment = projectFile.get('attach'); if (attachment) { // 如果attachment是Pointer对象,需要先获取完整的对象 if (typeof attachment.destroy === 'function') { await attachment.destroy(); } else if (attachment.id) { // 如果是Pointer,需要先获取完整的Attachment对象 const attachmentQuery = new Parse.Query("Attachment"); const fullAttachment = await attachmentQuery.get(attachment.id); await fullAttachment.destroy(); } } // 删除ProjectFile记录 await projectFile.destroy(); } catch (error) { console.error('删除项目文件失败:', error); throw error; } } /** * 获取项目文件列表 */ async getProjectFiles( projectId: string, filters?: { fileType?: string; spaceId?: string; stage?: string; } ): Promise { try { const ProjectFile = new Parse.Object('ProjectFile'); const query = new Parse.Query("ProjectFile"); // 关联项目查询 const Project = new Parse.Object('Project'); const projectQuery = new Parse.Query("Project"); projectQuery.equalTo('objectId', projectId); query.matchesQuery('project', projectQuery); query.include('attach', 'uploadedBy'); query.descending('createdAt'); // 应用过滤器 if (filters?.fileType) { query.equalTo('fileType', filters.fileType); } if (filters?.stage) { query.equalTo('stage', filters.stage); } const results = await query.find(); // 如果有空间ID过滤,从data中筛选 if (filters?.spaceId) { return results.filter(result => { const data = result.get('data'); return data?.spaceId === filters.spaceId; }); } return results; } catch (error) { console.error('获取项目文件列表失败:', error); throw error; } } /** * 批量上传文件 */ async uploadMultipleFiles( files: File[], projectId: string, fileType: string, spaceId?: string, stage?: string, onProgress?: (fileIndex: number, progress: number) => void ): Promise { const results: NovaFile[] = []; for (let i = 0; i < files.length; i++) { const file = files[i]; try { const uploadedFile = await this.uploadProjectFile( file, projectId, fileType, spaceId, stage, undefined, (progress) => { if (onProgress) { onProgress(i, progress); } } ); results.push(uploadedFile); } catch (error) { console.error(`文件 ${file.name} 上传失败:`, error); // 继续上传其他文件 } } return results; } /** * 验证文件 */ validateFile(file: File, maxSize: number = 50 * 1024 * 1024, allowedTypes?: string[]): boolean { // 检查文件大小 if (file.size > maxSize) { return false; } // 检查文件类型 if (allowedTypes && !allowedTypes.includes(file.type)) { return false; } return true; } /** * 获取文件类型标签 */ getFileTypeLabel(fileType: string): string { const typeMap: Record = { 'image/jpeg': '图片', 'image/png': '图片', 'image/gif': '图片', 'image/webp': '图片', 'video/mp4': '视频', 'video/mov': '视频', 'video/avi': '视频', 'application/pdf': 'PDF', 'application/msword': 'Word', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'Word', 'application/vnd.ms-excel': 'Excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'Excel', 'application/vnd.ms-powerpoint': 'PPT', 'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'PPT' }; return typeMap[fileType] || '其他'; } /** * 格式化文件大小 */ formatFileSize(bytes: number): string { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } /** * 上传文件并创建 Attachment 与 ProjectFile 记录,返回 ProjectFile */ async uploadProjectFileWithRecord( file: File, projectId: string, fileType: string, spaceId?: string, stage?: string, additionalMetadata?: any, onProgress?: (progress: number) => void ): Promise { // 🔥 添加重试机制 const maxRetries = 3; let lastError: any = null; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { console.log(`📤 上传尝试 ${attempt}/${maxRetries}: ${file.name}`); // 🔥 修复存储桶配置:使用默认存储桶 let cid = localStorage.getItem('company'); if (!cid) { console.warn('⚠️ 未找到公司ID,使用默认存储桶'); cid = 'cDL6R1hgSi'; // 默认公司ID } console.log(`📦 使用存储桶CID: ${cid}`); const storage = await NovaStorage.withCid(cid); let prefixKey = `project/${projectId}`; if (spaceId) { prefixKey += `/space/${spaceId}`; } if (stage) { prefixKey += `/stage/${stage}`; } console.log(`📤 开始上传文件: ${file.name}`, { size: `${(file.size / 1024 / 1024).toFixed(2)}MB`, type: file.type, prefixKey, projectId }); const uploadedFile = await storage.upload(file, { prefixKey, onProgress: (progress: { total: { percent: number } }) => { if (onProgress) { onProgress(progress.total.percent); } } }); console.log(`✅ 文件上传成功: ${file.name}`, { url: uploadedFile.url, key: uploadedFile.key }); const attachment = await this.saveToAttachmentTable( uploadedFile, projectId, fileType, spaceId, stage, additionalMetadata ); const projectFile = await this.saveToProjectFile( attachment, projectId, fileType, spaceId, stage ); return projectFile; } catch (error: any) { lastError = error; console.error(`❌ 上传尝试 ${attempt}/${maxRetries} 失败:`, error); console.error('❌ 错误详情:', { message: error?.message, code: error?.code || error?.status, name: error?.name, fileName: file.name, fileSize: `${(file.size / 1024 / 1024).toFixed(2)}MB`, projectId, attempt }); // 🔥 如果是631错误且还有重试次数,等待后重试 if ((error?.status === 631 || error?.code === 631) && attempt < maxRetries) { const waitTime = Math.min(1000 * Math.pow(2, attempt - 1), 5000); // 指数退避 console.log(`⏳ 等待 ${waitTime}ms 后重试...`); await new Promise(resolve => setTimeout(resolve, waitTime)); continue; } // 🔥 如果是最后一次尝试,抛出详细错误 if (attempt === maxRetries) { if (error?.status === 631 || error?.code === 631) { const errorMsg = `存储服务错误(631):${file.name}\n已重试${maxRetries}次\n\n可能原因:\n1. 存储配额已满(最可能)\n2. 项目ID无效: ${projectId}\n3. 存储服务暂时不可用\n4. 网络连接问题\n\n建议:\n- 联系管理员检查OBS存储配额\n- 稍后再试\n- 尝试上传更小的文件`; console.error('❌ 631错误(已重试):', errorMsg); throw new Error(errorMsg); } throw error; } } } // 不应该到达这里 throw lastError || new Error('上传失败'); } /** * 保存空间需求数据到ProjectFile表 */ async saveSpaceRequirements(projectId: string, spaceId: string, requirementsData: any): Promise { try { console.log(`💾 保存空间需求数据: ${spaceId}`, requirementsData); // 查找现有记录 const query = new Parse.Query('ProjectFile'); query.equalTo('project', { __type: 'Pointer', className: 'Project', objectId: projectId }); query.equalTo('fileType', 'space_requirements'); // 在data字段中查找匹配的spaceId const existingRecords = await query.find(); let existingRecord = null; for (const record of existingRecords) { const data = record.get('data'); if (data && data.spaceId === spaceId) { existingRecord = record; break; } } const dataToSave = { spaceId: spaceId, ...requirementsData, spaceRequirementsVersion: '1.0', lastUpdated: new Date().toISOString() }; if (existingRecord) { // 更新现有记录 existingRecord.set('data', dataToSave); await existingRecord.save(); console.log(`✅ 空间需求数据已更新: ${spaceId}`); } else { // 创建新记录 const projectFile = new Parse.Object('ProjectFile'); // 获取项目对象 const projectQuery = new Parse.Query('Project'); const project = await projectQuery.get(projectId); projectFile.set('project', project); projectFile.set('fileType', 'space_requirements'); projectFile.set('stage', 'requirements'); projectFile.set('data', dataToSave); // 设置上传者 const currentUser = Parse.User.current(); if (currentUser) { projectFile.set('uploadedBy', currentUser); } await projectFile.save(); console.log(`✅ 空间需求数据已创建: ${spaceId}`); } } catch (error) { console.error('保存空间需求数据失败:', error); throw error; } } }