| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675 |
- import { Injectable } from '@angular/core';
- import { FmodeParse } from 'fmode-ng/parse';
- import { CaseService } from '../../../services/case.service';
- const Parse = FmodeParse.with('nova');
- /**
- * 项目自动创建案例服务
- * 负责处理项目完成后自动同步到案例库
- */
- @Injectable({
- providedIn: 'root'
- })
- export class ProjectAutoCaseService {
- private companyId = localStorage.getItem('company')!;
- constructor(private caseService: CaseService) {}
- /**
- * 更新测试项目到售后归档阶段并创建案例
- */
- async updateTestProjectToAftercare(): Promise<{ success: boolean; message: string; caseId?: string }> {
- try {
- console.log('🔄 开始更新测试项目到售后归档...');
-
- // 1. 查找"10.28 测试项目"
- const ProjectQuery = new Parse.Query('Project');
- ProjectQuery.equalTo('company', { __type: 'Pointer', className: 'Company', objectId: this.companyId });
- ProjectQuery.equalTo('title', '10.28 测试项目');
- ProjectQuery.notEqualTo('isDeleted', true);
-
- const testProject = await ProjectQuery.first();
- if (!testProject) {
- return { success: false, message: '未找到"10.28 测试项目"' };
- }
-
- console.log('✅ 找到测试项目:', testProject.id);
-
- // 2. 查询项目的所有Product(空间)
- const ProductQuery = new Parse.Query('Product');
- ProductQuery.equalTo('project', testProject.toPointer());
- ProductQuery.notEqualTo('isDeleted', true);
- ProductQuery.include('profile');
-
- const products = await ProductQuery.find();
- console.log(`📦 找到 ${products.length} 个空间:`, products.map(p => p.get('productName')));
-
- // 3. 检查是否存在重复空间,如果有则清理
- const uniqueProducts = await this.cleanDuplicateProducts(testProject.id, products);
- console.log(`✅ 清理后剩余 ${uniqueProducts.length} 个唯一空间`);
-
- // 4. 为每个空间填充测试数据并更新到"售后归档"阶段
- for (const product of uniqueProducts) {
- await this.fillProductTestData(product);
- }
-
- // 5. 更新Project表的currentStage和status
- testProject.set('currentStage', '售后归档');
- testProject.set('status', '已完成');
- testProject.set('data', {
- ...testProject.get('data'),
- completedAt: new Date(),
- completedBy: 'admin',
- autoCompleted: true
- });
- await testProject.save();
- console.log('✅ 项目阶段已更新为"售后归档",状态为"已完成"');
-
- // 6. 自动创建案例
- const caseResult = await this.createCaseFromProject(testProject, uniqueProducts);
-
- if (caseResult.success) {
- return {
- success: true,
- message: `项目已更新到售后归档阶段!\n✅ 自动创建案例: ${caseResult.caseId}\n📦 包含 ${uniqueProducts.length} 个空间`,
- caseId: caseResult.caseId
- };
- } else {
- return {
- success: false,
- message: `项目更新成功,但案例创建失败: ${caseResult.error}`
- };
- }
-
- } catch (error) {
- console.error('❌ 更新测试项目失败:', error);
- return {
- success: false,
- message: `更新失败: ${error instanceof Error ? error.message : '未知错误'}`
- };
- }
- }
- /**
- * 扫描已完成但尚未生成案例的项目并补齐(幂等)
- */
- async backfillMissingCases(limit: number = 10): Promise<{ created: number; scanned: number }> {
- const ProjectQuery = new Parse.Query('Project');
- ProjectQuery.equalTo('company', { __type: 'Pointer', className: 'Company', objectId: this.companyId });
- ProjectQuery.notEqualTo('isDeleted', true);
- ProjectQuery.containedIn('currentStage', ['尾款结算', '售后归档', 'aftercare']);
- ProjectQuery.doesNotExist('data.caseId');
- ProjectQuery.include('contact', 'assignee', 'department'); // 🔥 必须 include 关联对象
- ProjectQuery.limit(limit);
- const projects = await ProjectQuery.find();
- let created = 0;
- for (const p of projects) {
- try {
- // 仅当满足售后归档同步条件时才创建案例
- const eligible = await this.isProjectEligibleForCase(p);
- if (!eligible) {
- continue;
- }
- const ProductQuery = new Parse.Query('Product');
- ProductQuery.equalTo('project', p.toPointer());
- ProductQuery.notEqualTo('isDeleted', true);
- ProductQuery.include('profile');
- const products = await ProductQuery.find();
- const res = await this.createCaseFromProject(p, products);
- if (res.success) created++;
- } catch (e) {
- console.warn('⚠️ 补齐案例失败', p.id, p.get('title'), e);
- }
- }
- return { created, scanned: projects.length };
- }
- /**
- * 对外公开:为指定项目创建案例(不变更项目阶段)
- * 在项目已进入"售后归档/尾款结算"等完成阶段时调用
- */
- async createCaseForProject(projectId: string): Promise<{ success: boolean; caseId?: string; error?: string }> {
- try {
- const ProjectQuery = new Parse.Query('Project');
- ProjectQuery.equalTo('company', { __type: 'Pointer', className: 'Company', objectId: this.companyId });
- ProjectQuery.include('contact', 'assignee', 'department'); // 🔥 必须 include 关联对象
- const project = await ProjectQuery.get(projectId);
- // 若已存在关联的案例,直接返回
- const data = project.get('data') || {};
- if (data.caseId) {
- return { success: true, caseId: data.caseId };
- }
- // 条件校验:售后归档同步条件(评价+支付凭证+复盘),尾款部分支付也允许
- const eligible = await this.isProjectEligibleForCase(project);
- if (!eligible) {
- return { success: false, error: '项目未满足售后归档同步条件(需评价、支付凭证、复盘)' };
- }
- // 加载项目的空间(Product)
- const ProductQuery = new Parse.Query('Product');
- ProductQuery.equalTo('project', project.toPointer());
- ProductQuery.notEqualTo('isDeleted', true);
- ProductQuery.include('profile');
- const products = await ProductQuery.find();
- return await this.createCaseFromProject(project, products);
- } catch (error) {
- return { success: false, error: error instanceof Error ? error.message : '未知错误' };
- }
- }
-
- /**
- * 清理重复的Product(空间)
- * 保留每个空间名称的第一个,删除其他重复的
- */
- private async cleanDuplicateProducts(projectId: string, products: any[]): Promise<any[]> {
- const uniqueMap = new Map<string, any>();
- const duplicates: any[] = [];
-
- // 按创建时间排序(保留最早创建的)
- const sortedProducts = products.sort((a, b) => {
- const timeA = a.get('createdAt')?.getTime() || 0;
- const timeB = b.get('createdAt')?.getTime() || 0;
- return timeA - timeB;
- });
-
- for (const product of sortedProducts) {
- const productName = product.get('productName') || '未命名';
-
- if (!uniqueMap.has(productName)) {
- uniqueMap.set(productName, product);
- } else {
- duplicates.push(product);
- }
- }
-
- // 软删除重复的Product
- if (duplicates.length > 0) {
- console.log(`🗑️ 发现 ${duplicates.length} 个重复空间,将进行软删除:`, duplicates.map(p => p.get('productName')));
-
- for (const duplicate of duplicates) {
- duplicate.set('isDeleted', true);
- duplicate.set('data', {
- ...duplicate.get('data'),
- deletedAt: new Date(),
- deletedReason: '重复空间,自动清理'
- });
- await duplicate.save();
- console.log(`✅ 已删除重复空间: ${duplicate.get('productName')} (${duplicate.id})`);
- }
- }
-
- return Array.from(uniqueMap.values());
- }
-
- /**
- * 为Product填充测试数据
- */
- private async fillProductTestData(product: any): Promise<void> {
- const productName = product.get('productName') || '未命名空间';
-
- // 更新阶段和状态
- product.set('stage', '售后归档');
- product.set('status', '已完成');
- product.set('reviewStatus', '已通过');
-
- // 填充测试数据
- const testData = {
- ...product.get('data'),
-
- // 空间信息
- space: {
- area: this.getRandomArea(productName),
- style: ['现代简约', '北欧风', '新中式', '轻奢风'][Math.floor(Math.random() * 4)],
- color: ['暖色调', '冷色调', '中性色'][Math.floor(Math.random() * 3)],
- lighting: '充足'
- },
-
- // 需求信息
- requirements: {
- customerNeeds: `${productName}需要${['温馨舒适', '简约大气', '实用美观'][Math.floor(Math.random() * 3)]}的设计风格`,
- specialRequests: ['环保材料', '智能家居', '收纳优化'][Math.floor(Math.random() * 3)],
- budget: this.getRandomBudget(productName)
- },
-
- // 设计进度
- designProgress: {
- modeling: { completed: true, completedAt: new Date(Date.now() - 20 * 24 * 60 * 60 * 1000) },
- rendering: { completed: true, completedAt: new Date(Date.now() - 15 * 24 * 60 * 60 * 1000) },
- postProduction: { completed: true, completedAt: new Date(Date.now() - 10 * 24 * 60 * 60 * 1000) },
- review: { completed: true, completedAt: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000), passed: true }
- },
-
- // 完成信息
- completedAt: new Date(),
- completedBy: product.get('profile')?.id || 'test-designer',
-
- // 图片资源(模拟)
- images: this.generateMockImages(productName),
-
- // 客户评价
- customerReview: {
- rating: 5,
- comment: `${productName}的设计非常满意,设计师很专业!`,
- createdAt: new Date()
- }
- };
-
- product.set('data', testData);
-
- // 报价信息
- if (!product.get('quotation')) {
- product.set('quotation', {
- total: testData.requirements.budget,
- designFee: Math.round(testData.requirements.budget * 0.2),
- materialFee: Math.round(testData.requirements.budget * 0.5),
- constructionFee: Math.round(testData.requirements.budget * 0.3),
- status: 'approved'
- });
- }
-
- await product.save();
- console.log(`✅ ${productName} 测试数据填充完成`);
- }
-
- /**
- * 根据空间名称生成随机面积
- */
- private getRandomArea(spaceName: string): number {
- const areaMap: Record<string, [number, number]> = {
- '主卧': [15, 25],
- '书房': [8, 15],
- '儿童房': [10, 18],
- '卫生间': [4, 8],
- '厨房': [6, 12],
- '客厅': [25, 40],
- '餐厅': [12, 20]
- };
-
- const [min, max] = areaMap[spaceName] || [10, 20];
- return Math.floor(Math.random() * (max - min + 1)) + min;
- }
-
- /**
- * 根据空间名称生成随机预算
- */
- private getRandomBudget(spaceName: string): number {
- const area = this.getRandomArea(spaceName);
- const pricePerSqm = 800 + Math.floor(Math.random() * 400); // 800-1200/平米
- return Math.round(area * pricePerSqm / 100) * 100; // 四舍五入到百位
- }
-
- /**
- * 生成模拟图片URL(使用真实的家装设计图片)
- */
- private generateMockImages(spaceName: string): string[] {
- // 使用Unsplash的高质量家装设计图片
- const defaultInteriorImages = [
- 'https://images.unsplash.com/photo-1600210492486-724fe5c67fb0?w=800&h=600&fit=crop', // 现代客厅
- 'https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?w=800&h=600&fit=crop', // 简约卧室
- 'https://images.unsplash.com/photo-1600566753190-17f0baa2a6c3?w=800&h=600&fit=crop', // 厨房设计
- 'https://images.unsplash.com/photo-1600585154340-be6161a56a0c?w=800&h=600&fit=crop', // 餐厅空间
- 'https://images.unsplash.com/photo-1600566753086-00f18fb6b3ea?w=800&h=600&fit=crop', // 书房设计
- 'https://images.unsplash.com/photo-1600573472550-8090b5e0745e?w=800&h=600&fit=crop', // 浴室设计
- 'https://images.unsplash.com/photo-1600585154526-990dced4db0d?w=800&h=600&fit=crop', // 儿童房
- 'https://images.unsplash.com/photo-1600566752355-35792bedcfea?w=800&h=600&fit=crop' // 阳台设计
- ];
-
- // 根据空间名称选择合适的图片
- const spaceImageMap: Record<string, number[]> = {
- '客厅': [0, 1],
- '主卧': [1, 2],
- '书房': [4, 0],
- '儿童房': [6, 1],
- '卫生间': [5, 3],
- '厨房': [2, 3],
- '餐厅': [3, 0]
- };
-
- const indices = spaceImageMap[spaceName] || [0, 1];
- return [
- defaultInteriorImages[indices[0]],
- defaultInteriorImages[indices[1]],
- defaultInteriorImages[(indices[0] + 2) % defaultInteriorImages.length]
- ];
- }
-
- /**
- * 从Project和Products创建Case(完整版本)
- */
- private async createCaseFromProject(project: any, products: any[]): Promise<{ success: boolean; caseId?: string; error?: string }> {
- try {
- // 获取项目信息
- const contact = project.get('contact');
- const assignee = project.get('assignee');
- const department = project.get('department');
- const projectData = project.get('data') || {};
-
- // 收集所有空间的图片和交付文件
- const allImages: string[] = [];
- let totalPrice = 0;
- let totalArea = 0;
-
- // 默认家装图片(如果没有上传图片则使用)
- const defaultInteriorImages = [
- 'https://images.unsplash.com/photo-1600210492486-724fe5c67fb0?w=800&h=600&fit=crop', // 现代客厅
- 'https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?w=800&h=600&fit=crop', // 简约卧室
- 'https://images.unsplash.com/photo-1600566753190-17f0baa2a6c3?w=800&h=600&fit=crop', // 厨房设计
- 'https://images.unsplash.com/photo-1600585154340-be6161a56a0c?w=800&h=600&fit=crop', // 餐厅空间
- 'https://images.unsplash.com/photo-1600566753086-00f18fb6b3ea?w=800&h=600&fit=crop', // 书房设计
- 'https://images.unsplash.com/photo-1600573472550-8090b5e0745e?w=800&h=600&fit=crop', // 浴室设计
- 'https://images.unsplash.com/photo-1600585154526-990dced4db0d?w=800&h=600&fit=crop', // 儿童房
- 'https://images.unsplash.com/photo-1600566752355-35792bedcfea?w=800&h=600&fit=crop' // 阳台设计
- ];
-
- // 从Product(空间)收集数据
- const productsDetail: any[] = [];
- for (const product of products) {
- const productData = product.get('data') || {};
- if (productData.images && Array.isArray(productData.images)) {
- allImages.push(...productData.images);
- }
-
- const quotation = product.get('quotation');
- if (quotation && quotation.total) {
- totalPrice += quotation.total;
- }
-
- const spaceArea = productData.space?.area || 0;
- if (spaceArea > 0) {
- totalArea += spaceArea;
- }
-
- // 产品详细信息
- productsDetail.push({
- productId: product.id,
- productName: product.get('productName') || '未命名空间',
- category: productData.space?.type || '其他',
- brand: '',
- quantity: 1,
- unitPrice: quotation?.total || 0,
- totalPrice: quotation?.total || 0,
- spaceArea: spaceArea,
- specifications: {
- style: productData.space?.style || '现代简约',
- color: productData.space?.color || '',
- lighting: productData.space?.lighting || '',
- requirements: productData.requirements || {}
- }
- });
- }
-
- // 从ProjectFile加载交付文件图片
- try {
- const Parse = await import('fmode-ng/parse').then(m => m.FmodeParse.with('nova'));
- const fileQuery = new Parse.Query('ProjectFile');
- fileQuery.equalTo('project', project.toPointer());
- fileQuery.equalTo('stage', 'delivery');
- fileQuery.include('attach');
- fileQuery.limit(100);
- const deliveryFiles = await fileQuery.find();
-
- const imagesDetail: any = {
- beforeRenovation: [],
- afterRenovation: [],
- videos: [],
- panoramas: []
- };
-
- for (const file of deliveryFiles) {
- const attach = file.get('attach');
- if (!attach) continue;
-
- const fileUrl = attach.get('url') || '';
- const fileName = attach.get('name') || attach.get('originalName') || '';
- const fileType = attach.get('mime') || attach.get('type') || '';
- const fileData = file.get('data') || {};
-
- const fileInfo = {
- attachmentId: attach.id || '',
- url: fileUrl,
- description: fileName,
- uploadDate: file.get('createdAt')?.toISOString() || new Date().toISOString(),
- spaceArea: fileData.productName || fileData.spaceName || ''
- };
-
- // 根据文件类型分类
- if (fileType.startsWith('image/')) {
- allImages.push(fileUrl);
- // 根据fileType判断是before/after
- const fileTypeStr = file.get('fileType') || '';
- if (fileTypeStr.includes('before')) {
- imagesDetail.beforeRenovation.push(fileInfo);
- } else {
- imagesDetail.afterRenovation.push(fileInfo);
- }
- } else if (fileType.startsWith('video/')) {
- imagesDetail.videos.push({
- ...fileInfo,
- duration: 0
- });
- }
- }
-
- // 保存图片详细信息到data
- projectData.imagesDetail = imagesDetail;
- } catch (e) {
- console.warn('⚠️ 加载交付文件失败(不影响案例创建):', e);
- }
-
- // 从项目data中提取标签
- const styleTags = projectData.quotation?.spaces?.map((s: any) =>
- s.name || ''
- ).filter(Boolean) || [];
- const defaultTags = ['全屋设计', '专业团队'];
- const finalTags = [...new Set([...styleTags, ...defaultTags])].slice(0, 5);
-
- // 如果没有上传图片,使用默认家装图片
- const finalImages = allImages.length > 0 ? allImages : defaultInteriorImages;
- const finalCoverImage = finalImages[0] || defaultInteriorImages[0];
-
- console.log(`📸 案例图片: ${allImages.length > 0 ? '使用上传的图片' : '使用默认家装图片'}, 共 ${finalImages.length} 张`);
-
- // 构建案例数据
- const caseData = {
- name: project.get('title') || '未命名案例',
- projectId: project.id,
- designerId: assignee?.id || '',
- teamId: department?.id || '',
- coverImage: finalCoverImage,
- images: finalImages.slice(0, 20), // 最多20张图片
- totalPrice: totalPrice || projectData.quotation?.total || 0,
- completionDate: projectData.deliveryCompletedAt || projectData.completedAt || new Date(),
- tag: finalTags,
- info: {
- area: totalArea || projectData.quotation?.spaces?.reduce((sum: number, s: any) => sum + (s.area || 0), 0) || 100,
- projectType: (project.get('projectType') || '家装') as '工装' | '家装',
- roomType: (projectData.roomType || '三居室') as '一居室' | '二居室' | '三居室' | '四居+',
- spaceType: (projectData.spaceType || project.get('spaceType') || '平层') as '平层' | '复式' | '别墅' | '自建房',
- renderingLevel: (project.get('renderType') === '360全景' ? '高端' : projectData.priceLevel === '三级' ? '高端' : projectData.priceLevel === '二级' ? '中端' : '低端') as '高端' | '中端' | '低端'
- },
- isPublished: true,
- publishedAt: new Date(),
- isExcellent: false,
- index: Math.floor(Date.now() / 1000), // 使用时间戳作为排序,新案例靠前
- customerReview: projectData.customerReview || `客户${(contact && typeof contact.get === 'function') ? contact.get('name') : (contact?.name || '')}对整体设计非常满意,设计师团队专业高效!`,
- data: {
- // 装修规格信息
- renovationSpec: projectData.renovationSpec || undefined,
-
- // 产品详细信息
- productsDetail,
-
- // 预算信息
- budget: {
- total: totalPrice,
- designFee: Math.round(totalPrice * 0.2),
- constructionFee: Math.round(totalPrice * 0.5),
- softDecorFee: Math.round(totalPrice * 0.3),
- actualCost: totalPrice,
- savingRate: 0
- },
-
- // 时间线
- timeline: {
- startDate: project.get('createdAt')?.toISOString() || new Date().toISOString(),
- completionDate: (projectData.deliveryCompletedAt || projectData.completedAt || new Date()).toISOString(),
- duration: Math.ceil((new Date().getTime() - (project.get('createdAt')?.getTime() || Date.now())) / (1000 * 60 * 60 * 24)),
- milestones: [
- { stage: '订单分配', date: project.get('createdAt')?.toISOString() || '', status: '已完成' },
- { stage: '确认需求', date: projectData.requirementsConfirmedAt?.toISOString() || '', status: '已完成' },
- { stage: '交付执行', date: projectData.deliveryStartedAt?.toISOString() || '', status: '已完成' },
- { stage: '售后归档', date: projectData.deliveryCompletedAt?.toISOString() || new Date().toISOString(), status: '已完成' }
- ].filter(m => m.date)
- },
-
- // 设计亮点
- highlights: projectData.highlights || [
- '空间布局合理,动线流畅',
- '自然采光充足,通风良好',
- '收纳设计巧妙,实用美观',
- '色彩搭配和谐,氛围温馨'
- ],
-
- // 设计特色
- features: projectData.features || [],
-
- // 设计挑战
- challenges: projectData.challenges || [],
-
- // 材料信息
- materials: projectData.materials || {},
-
- // 客户信息
- clientInfo: {
- familyMembers: (contact && typeof contact.get === 'function') ? (contact.get('data')?.familyMembers || 3) : 3,
- ageRange: (contact && typeof contact.get === 'function') ? (contact.get('data')?.ageRange || '30-40岁') : '30-40岁',
- lifestyle: (contact && typeof contact.get === 'function') ? (contact.get('data')?.lifestyle || '现代都市') : '现代都市',
- specialNeeds: (contact && typeof contact.get === 'function') ? (contact.get('data')?.specialNeeds || []) : [],
- satisfactionScore: projectData.customerReview?.rating || 5
- },
-
- // 施工团队信息
- constructionTeam: projectData.constructionTeam || {
- contractor: (department && typeof department.get === 'function') ? department.get('name') : (department?.name || '设计团队'),
- projectManager: (assignee && typeof assignee.get === 'function') ? assignee.get('name') : (assignee?.name || ''),
- supervisor: '',
- workers: products.length,
- subcontractors: []
- },
-
- // 质量验收
- qualityInspection: projectData.qualityInspection || undefined,
-
- // 图片详细信息
- imagesDetail: projectData.imagesDetail || {
- beforeRenovation: [],
- afterRenovation: finalImages.map((url, idx) => ({
- attachmentId: `img-${idx}`,
- url,
- description: allImages.length > 0 ? `效果图${idx + 1}` : `家装设计参考${idx + 1}`,
- uploadDate: new Date().toISOString(),
- spaceArea: products[idx % products.length]?.get('productName') || ''
- })),
- videos: [],
- panoramas: []
- }
- }
- };
-
- // 调用CaseService创建案例
- const newCase = await this.caseService.createCase(caseData);
-
- console.log('✅ 案例创建成功:', newCase.id, '项目:', project.get('title'));
-
- // 在Project的data中记录关联的案例ID
- project.set('data', {
- ...project.get('data'),
- caseId: newCase.id,
- caseCreatedAt: new Date()
- });
- await project.save();
-
- return { success: true, caseId: newCase.id };
-
- } catch (error) {
- console.error('❌ 创建案例失败:', error);
- return {
- success: false,
- error: error instanceof Error ? error.message : '未知错误'
- };
- }
- }
- /**
- * 判断项目是否满足同步到案例库的条件:
- * - 当前阶段为售后归档/aftercare/尾款结算 或 状态已归档
- * - 有客户评价(ProjectFeedback)
- * - 有支付凭证(ProjectPayment[type=final,status=paid] 或 ProjectFile(stage=aftercare,fileType=payment_voucher))
- * - 有项目复盘(project.data.retrospective)
- * - 尾款允许部分支付(只要存在任意尾款支付记录或凭证即可)
- */
- private async isProjectEligibleForCase(project: any): Promise<boolean> {
- try {
- const stage: string = project.get('currentStage') || '';
- const status: string = project.get('status') || '';
- const data = project.get('data') || {};
- const inAftercare = ['售后归档', 'aftercare', '尾款结算'].some(s => stage.includes(s)) || status === '已归档';
- if (!inAftercare) return false;
- // 评价:至少有一条 ProjectFeedback
- const fbQuery = new Parse.Query('ProjectFeedback');
- fbQuery.equalTo('project', project.toPointer());
- fbQuery.notEqualTo('isDeleted', true);
- const feedbackCount = await fbQuery.count();
- if (feedbackCount <= 0) return false;
- // 复盘:存在 data.retrospective 对象或标记
- const hasRetrospective = !!(data.retrospective && (data.retrospective.generated !== false));
- if (!hasRetrospective) return false;
- // 支付凭证:优先查 ProjectPayment(允许部分支付),再降级查 ProjectFile(payment_voucher)
- let hasVoucher = false;
- try {
- // 允许部分支付:存在任意记录即可;优先匹配已付款
- const paidQuery = new Parse.Query('ProjectPayment');
- paidQuery.equalTo('project', project.toPointer());
- paidQuery.equalTo('type', 'final');
- paidQuery.notEqualTo('isDeleted', true);
- paidQuery.equalTo('status', 'paid');
- const paidCount = await paidQuery.count();
- if (paidCount > 0) hasVoucher = true;
- if (!hasVoucher) {
- const anyQuery = new Parse.Query('ProjectPayment');
- anyQuery.equalTo('project', project.toPointer());
- anyQuery.equalTo('type', 'final');
- anyQuery.notEqualTo('isDeleted', true);
- const anyCount = await anyQuery.count();
- hasVoucher = anyCount > 0;
- }
- } catch (e) {
- // 忽略类不存在错误
- }
- if (!hasVoucher) {
- const fileQuery = new Parse.Query('ProjectFile');
- fileQuery.equalTo('project', project.toPointer());
- fileQuery.equalTo('stage', 'aftercare');
- fileQuery.equalTo('fileType', 'payment_voucher');
- const fileCount = await fileQuery.count();
- hasVoucher = fileCount > 0;
- }
- return hasVoucher;
- } catch (e) {
- console.warn('⚠️ 案例同步条件检测失败,默认不创建:', e);
- return false;
- }
- }
- }
|