| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541 |
- 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<NovaFile> {
- 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<FmodeObject> {
- 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<FmodeObject> {
- 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<void> {
- 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<FmodeObject[]> {
- 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<NovaFile[]> {
- 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<string, string> = {
- '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<FmodeObject> {
- // 🔥 添加重试机制
- 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<void> {
- 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;
- }
- }
- }
|