|
|
@@ -0,0 +1,899 @@
|
|
|
+import { Injectable } from '@angular/core';
|
|
|
+import { FmodeParse, FmodeObject } from 'fmode-ng/parse';
|
|
|
+import { PaymentVoucherAIService } from './payment-voucher-ai.service';
|
|
|
+import { ProjectFileService } from './project-file.service';
|
|
|
+
|
|
|
+const Parse = FmodeParse.with('nova');
|
|
|
+
|
|
|
+/**
|
|
|
+ * 售后归档数据服务
|
|
|
+ * 对接Parse Server数据库,管理ProjectPayment和ProjectFeedback数据
|
|
|
+ */
|
|
|
+@Injectable({
|
|
|
+ providedIn: 'root'
|
|
|
+})
|
|
|
+export class AftercareDataService {
|
|
|
+
|
|
|
+ constructor(
|
|
|
+ private paymentVoucherAI: PaymentVoucherAIService,
|
|
|
+ private projectFileService: ProjectFileService
|
|
|
+ ) {}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * ==================== 尾款管理相关 ====================
|
|
|
+ */
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取项目的所有付款记录
|
|
|
+ * @param projectId 项目ID
|
|
|
+ * @returns 付款记录列表
|
|
|
+ */
|
|
|
+ async getProjectPayments(projectId: string): Promise<FmodeObject[]> {
|
|
|
+ try {
|
|
|
+ const query = new Parse.Query('ProjectPayment');
|
|
|
+ query.equalTo('project', {
|
|
|
+ __type: 'Pointer',
|
|
|
+ className: 'Project',
|
|
|
+ objectId: projectId
|
|
|
+ });
|
|
|
+ query.notEqualTo('isDeleted', true);
|
|
|
+ query.include('voucherFile');
|
|
|
+ query.include('paidBy');
|
|
|
+ query.include('recordedBy');
|
|
|
+ query.include('verifiedBy');
|
|
|
+ query.include('product');
|
|
|
+ query.descending('createdAt');
|
|
|
+
|
|
|
+ const payments = await query.find();
|
|
|
+ console.log(`✅ 获取项目 ${projectId} 的付款记录,共 ${payments.length} 条`);
|
|
|
+ return payments;
|
|
|
+ } catch (error) {
|
|
|
+ console.error('❌ 获取项目付款记录失败:', error);
|
|
|
+ throw error;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取项目的尾款记录
|
|
|
+ * @param projectId 项目ID
|
|
|
+ * @returns 尾款记录列表
|
|
|
+ */
|
|
|
+ async getFinalPayments(projectId: string): Promise<FmodeObject[]> {
|
|
|
+ try {
|
|
|
+ const query = new Parse.Query('ProjectPayment');
|
|
|
+ query.equalTo('project', {
|
|
|
+ __type: 'Pointer',
|
|
|
+ className: 'Project',
|
|
|
+ objectId: projectId
|
|
|
+ });
|
|
|
+ query.equalTo('type', 'final'); // 只获取尾款类型
|
|
|
+ query.notEqualTo('isDeleted', true);
|
|
|
+ query.include('voucherFile');
|
|
|
+ query.include('paidBy');
|
|
|
+ query.include('recordedBy');
|
|
|
+ query.include('verifiedBy');
|
|
|
+ query.include('product');
|
|
|
+ query.descending('createdAt');
|
|
|
+
|
|
|
+ const payments = await query.find();
|
|
|
+ console.log(`✅ 获取项目 ${projectId} 的尾款记录,共 ${payments.length} 条`);
|
|
|
+ return payments;
|
|
|
+ } catch (error) {
|
|
|
+ console.error('❌ 获取项目尾款记录失败:', error);
|
|
|
+ throw error;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算项目的付款统计信息
|
|
|
+ * @param projectId 项目ID
|
|
|
+ * @returns 付款统计
|
|
|
+ */
|
|
|
+ async getPaymentStatistics(projectId: string): Promise<{
|
|
|
+ totalAmount: number;
|
|
|
+ paidAmount: number;
|
|
|
+ remainingAmount: number;
|
|
|
+ advanceAmount: number;
|
|
|
+ finalAmount: number;
|
|
|
+ status: 'pending' | 'partial' | 'completed' | 'overdue';
|
|
|
+ }> {
|
|
|
+ try {
|
|
|
+ const payments = await this.getProjectPayments(projectId);
|
|
|
+
|
|
|
+ let totalAmount = 0;
|
|
|
+ let paidAmount = 0;
|
|
|
+ let advanceAmount = 0;
|
|
|
+ let finalAmount = 0;
|
|
|
+ let hasOverdue = false;
|
|
|
+
|
|
|
+ const now = new Date();
|
|
|
+
|
|
|
+ for (const payment of payments) {
|
|
|
+ const amount = payment.get('amount') || 0;
|
|
|
+ const type = payment.get('type') || '';
|
|
|
+ const status = payment.get('status') || 'pending';
|
|
|
+ const dueDate = payment.get('dueDate');
|
|
|
+
|
|
|
+ totalAmount += amount;
|
|
|
+
|
|
|
+ if (status === 'paid') {
|
|
|
+ paidAmount += amount;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (type === 'advance') {
|
|
|
+ advanceAmount += amount;
|
|
|
+ } else if (type === 'final') {
|
|
|
+ finalAmount += amount;
|
|
|
+
|
|
|
+ // 检查尾款是否逾期
|
|
|
+ if (status === 'pending' && dueDate && dueDate < now) {
|
|
|
+ hasOverdue = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const remainingAmount = totalAmount - paidAmount;
|
|
|
+
|
|
|
+ // 判断状态
|
|
|
+ let paymentStatus: 'pending' | 'partial' | 'completed' | 'overdue' = 'pending';
|
|
|
+ if (hasOverdue) {
|
|
|
+ paymentStatus = 'overdue';
|
|
|
+ } else if (paidAmount >= totalAmount) {
|
|
|
+ paymentStatus = 'completed';
|
|
|
+ } else if (paidAmount > 0) {
|
|
|
+ paymentStatus = 'partial';
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log(`✅ 项目 ${projectId} 付款统计:`, {
|
|
|
+ totalAmount,
|
|
|
+ paidAmount,
|
|
|
+ remainingAmount,
|
|
|
+ advanceAmount,
|
|
|
+ finalAmount,
|
|
|
+ status: paymentStatus
|
|
|
+ });
|
|
|
+
|
|
|
+ return {
|
|
|
+ totalAmount,
|
|
|
+ paidAmount,
|
|
|
+ remainingAmount,
|
|
|
+ advanceAmount,
|
|
|
+ finalAmount,
|
|
|
+ status: paymentStatus
|
|
|
+ };
|
|
|
+ } catch (error) {
|
|
|
+ console.error('❌ 计算付款统计失败:', error);
|
|
|
+ throw error;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 创建付款记录
|
|
|
+ * @param paymentData 付款数据
|
|
|
+ * @returns 创建的付款记录
|
|
|
+ */
|
|
|
+ async createPayment(paymentData: {
|
|
|
+ projectId: string;
|
|
|
+ companyId: string;
|
|
|
+ type: 'advance' | 'milestone' | 'final' | 'refund';
|
|
|
+ stage: string;
|
|
|
+ method: string;
|
|
|
+ amount: number;
|
|
|
+ currency?: string;
|
|
|
+ percentage?: number;
|
|
|
+ dueDate?: Date;
|
|
|
+ paidById?: string;
|
|
|
+ recordedById: string;
|
|
|
+ description?: string;
|
|
|
+ notes?: string;
|
|
|
+ productId?: string;
|
|
|
+ }): Promise<FmodeObject> {
|
|
|
+ try {
|
|
|
+ const ProjectPayment = Parse.Object.extend('ProjectPayment');
|
|
|
+ const payment = new ProjectPayment();
|
|
|
+
|
|
|
+ // 必填字段
|
|
|
+ payment.set('project', {
|
|
|
+ __type: 'Pointer',
|
|
|
+ className: 'Project',
|
|
|
+ objectId: paymentData.projectId
|
|
|
+ });
|
|
|
+ payment.set('company', {
|
|
|
+ __type: 'Pointer',
|
|
|
+ className: 'Company',
|
|
|
+ objectId: paymentData.companyId
|
|
|
+ });
|
|
|
+ payment.set('type', paymentData.type);
|
|
|
+ payment.set('stage', paymentData.stage);
|
|
|
+ payment.set('method', paymentData.method);
|
|
|
+ payment.set('amount', paymentData.amount);
|
|
|
+ payment.set('currency', paymentData.currency || 'CNY');
|
|
|
+ payment.set('status', 'pending');
|
|
|
+ payment.set('recordedBy', {
|
|
|
+ __type: 'Pointer',
|
|
|
+ className: 'Profile',
|
|
|
+ objectId: paymentData.recordedById
|
|
|
+ });
|
|
|
+ payment.set('recordedDate', new Date());
|
|
|
+
|
|
|
+ // 可选字段
|
|
|
+ if (paymentData.percentage !== undefined) {
|
|
|
+ payment.set('percentage', paymentData.percentage);
|
|
|
+ }
|
|
|
+ if (paymentData.dueDate) {
|
|
|
+ payment.set('dueDate', paymentData.dueDate);
|
|
|
+ }
|
|
|
+ if (paymentData.paidById) {
|
|
|
+ payment.set('paidBy', {
|
|
|
+ __type: 'Pointer',
|
|
|
+ className: 'ContactInfo',
|
|
|
+ objectId: paymentData.paidById
|
|
|
+ });
|
|
|
+ }
|
|
|
+ if (paymentData.description) {
|
|
|
+ payment.set('description', paymentData.description);
|
|
|
+ }
|
|
|
+ if (paymentData.notes) {
|
|
|
+ payment.set('notes', paymentData.notes);
|
|
|
+ }
|
|
|
+ if (paymentData.productId) {
|
|
|
+ payment.set('product', {
|
|
|
+ __type: 'Pointer',
|
|
|
+ className: 'Product',
|
|
|
+ objectId: paymentData.productId
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ payment.set('isDeleted', false);
|
|
|
+
|
|
|
+ const savedPayment = await payment.save();
|
|
|
+ console.log(`✅ 创建付款记录成功:`, savedPayment.id);
|
|
|
+ return savedPayment;
|
|
|
+ } catch (error) {
|
|
|
+ console.error('❌ 创建付款记录失败:', error);
|
|
|
+ throw error;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 更新付款记录状态
|
|
|
+ * @param paymentId 付款记录ID
|
|
|
+ * @param status 新状态
|
|
|
+ * @param voucherFileId 付款凭证文件ID(可选)
|
|
|
+ * @returns 更新后的付款记录
|
|
|
+ */
|
|
|
+ async updatePaymentStatus(
|
|
|
+ paymentId: string,
|
|
|
+ status: 'pending' | 'paid' | 'overdue' | 'cancelled',
|
|
|
+ voucherFileId?: string
|
|
|
+ ): Promise<FmodeObject> {
|
|
|
+ try {
|
|
|
+ const query = new Parse.Query('ProjectPayment');
|
|
|
+ const payment = await query.get(paymentId);
|
|
|
+
|
|
|
+ payment.set('status', status);
|
|
|
+
|
|
|
+ if (status === 'paid') {
|
|
|
+ payment.set('paymentDate', new Date());
|
|
|
+ }
|
|
|
+
|
|
|
+ if (voucherFileId) {
|
|
|
+ payment.set('voucherFile', {
|
|
|
+ __type: 'Pointer',
|
|
|
+ className: 'ProjectFile',
|
|
|
+ objectId: voucherFileId
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ const savedPayment = await payment.save();
|
|
|
+ console.log(`✅ 更新付款记录状态成功: ${paymentId} -> ${status}`);
|
|
|
+ return savedPayment;
|
|
|
+ } catch (error) {
|
|
|
+ console.error('❌ 更新付款记录状态失败:', error);
|
|
|
+ throw error;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * ==================== 客户评价相关 ====================
|
|
|
+ */
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取项目的客户反馈
|
|
|
+ * @param projectId 项目ID
|
|
|
+ * @returns 反馈记录列表
|
|
|
+ */
|
|
|
+ async getProjectFeedbacks(projectId: string): Promise<FmodeObject[]> {
|
|
|
+ try {
|
|
|
+ const query = new Parse.Query('ProjectFeedback');
|
|
|
+ query.equalTo('project', {
|
|
|
+ __type: 'Pointer',
|
|
|
+ className: 'Project',
|
|
|
+ objectId: projectId
|
|
|
+ });
|
|
|
+ query.notEqualTo('isDeleted', true);
|
|
|
+ query.include('contact');
|
|
|
+ query.include('product');
|
|
|
+ query.descending('createdAt');
|
|
|
+
|
|
|
+ const feedbacks = await query.find();
|
|
|
+ console.log(`✅ 获取项目 ${projectId} 的客户反馈,共 ${feedbacks.length} 条`);
|
|
|
+ return feedbacks;
|
|
|
+ } catch (error) {
|
|
|
+ console.error('❌ 获取项目客户反馈失败:', error);
|
|
|
+ throw error;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取项目的综合评价统计
|
|
|
+ * @param projectId 项目ID
|
|
|
+ * @returns 评价统计
|
|
|
+ */
|
|
|
+ async getFeedbackStatistics(projectId: string): Promise<{
|
|
|
+ submitted: boolean;
|
|
|
+ overallRating: number;
|
|
|
+ totalFeedbacks: number;
|
|
|
+ averageRating: number;
|
|
|
+ dimensionRatings: {
|
|
|
+ designQuality: number;
|
|
|
+ serviceAttitude: number;
|
|
|
+ deliveryTimeliness: number;
|
|
|
+ valueForMoney: number;
|
|
|
+ communication: number;
|
|
|
+ };
|
|
|
+ productRatings: Array<{
|
|
|
+ productId: string;
|
|
|
+ productName: string;
|
|
|
+ rating: number;
|
|
|
+ comments: string;
|
|
|
+ }>;
|
|
|
+ }> {
|
|
|
+ try {
|
|
|
+ const feedbacks = await this.getProjectFeedbacks(projectId);
|
|
|
+
|
|
|
+ if (feedbacks.length === 0) {
|
|
|
+ return {
|
|
|
+ submitted: false,
|
|
|
+ overallRating: 0,
|
|
|
+ totalFeedbacks: 0,
|
|
|
+ averageRating: 0,
|
|
|
+ dimensionRatings: {
|
|
|
+ designQuality: 0,
|
|
|
+ serviceAttitude: 0,
|
|
|
+ deliveryTimeliness: 0,
|
|
|
+ valueForMoney: 0,
|
|
|
+ communication: 0
|
|
|
+ },
|
|
|
+ productRatings: []
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ // 计算平均评分
|
|
|
+ let totalRating = 0;
|
|
|
+ const dimensionSums = {
|
|
|
+ designQuality: 0,
|
|
|
+ serviceAttitude: 0,
|
|
|
+ deliveryTimeliness: 0,
|
|
|
+ valueForMoney: 0,
|
|
|
+ communication: 0
|
|
|
+ };
|
|
|
+ const dimensionCounts = {
|
|
|
+ designQuality: 0,
|
|
|
+ serviceAttitude: 0,
|
|
|
+ deliveryTimeliness: 0,
|
|
|
+ valueForMoney: 0,
|
|
|
+ communication: 0
|
|
|
+ };
|
|
|
+
|
|
|
+ const productRatingsMap: Map<string, {
|
|
|
+ productName: string;
|
|
|
+ ratings: number[];
|
|
|
+ comments: string[];
|
|
|
+ }> = new Map();
|
|
|
+
|
|
|
+ for (const feedback of feedbacks) {
|
|
|
+ const rating = feedback.get('rating') || 0;
|
|
|
+ totalRating += rating;
|
|
|
+
|
|
|
+ // 处理维度评分
|
|
|
+ const data = feedback.get('data') || {};
|
|
|
+ const dimensions = data.dimensionRatings || {};
|
|
|
+
|
|
|
+ for (const key of Object.keys(dimensionSums)) {
|
|
|
+ if (dimensions[key]) {
|
|
|
+ dimensionSums[key] += dimensions[key];
|
|
|
+ dimensionCounts[key]++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理产品评分
|
|
|
+ const product = feedback.get('product');
|
|
|
+ if (product) {
|
|
|
+ const productId = product.id;
|
|
|
+ const productName = product.get('name') || '未知产品';
|
|
|
+
|
|
|
+ if (!productRatingsMap.has(productId)) {
|
|
|
+ productRatingsMap.set(productId, {
|
|
|
+ productName,
|
|
|
+ ratings: [],
|
|
|
+ comments: []
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ const productData = productRatingsMap.get(productId)!;
|
|
|
+ productData.ratings.push(rating);
|
|
|
+
|
|
|
+ const comment = feedback.get('content') || '';
|
|
|
+ if (comment) {
|
|
|
+ productData.comments.push(comment);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const averageRating = totalRating / feedbacks.length;
|
|
|
+
|
|
|
+ // 计算维度平均分
|
|
|
+ const dimensionRatings = {
|
|
|
+ designQuality: dimensionCounts.designQuality > 0
|
|
|
+ ? dimensionSums.designQuality / dimensionCounts.designQuality
|
|
|
+ : 0,
|
|
|
+ serviceAttitude: dimensionCounts.serviceAttitude > 0
|
|
|
+ ? dimensionSums.serviceAttitude / dimensionCounts.serviceAttitude
|
|
|
+ : 0,
|
|
|
+ deliveryTimeliness: dimensionCounts.deliveryTimeliness > 0
|
|
|
+ ? dimensionSums.deliveryTimeliness / dimensionCounts.deliveryTimeliness
|
|
|
+ : 0,
|
|
|
+ valueForMoney: dimensionCounts.valueForMoney > 0
|
|
|
+ ? dimensionSums.valueForMoney / dimensionCounts.valueForMoney
|
|
|
+ : 0,
|
|
|
+ communication: dimensionCounts.communication > 0
|
|
|
+ ? dimensionSums.communication / dimensionCounts.communication
|
|
|
+ : 0
|
|
|
+ };
|
|
|
+
|
|
|
+ // 计算产品评分
|
|
|
+ const productRatings = Array.from(productRatingsMap.entries()).map(([productId, data]) => ({
|
|
|
+ productId,
|
|
|
+ productName: data.productName,
|
|
|
+ rating: data.ratings.reduce((a, b) => a + b, 0) / data.ratings.length,
|
|
|
+ comments: data.comments.join('; ')
|
|
|
+ }));
|
|
|
+
|
|
|
+ console.log(`✅ 项目 ${projectId} 评价统计:`, {
|
|
|
+ totalFeedbacks: feedbacks.length,
|
|
|
+ averageRating,
|
|
|
+ dimensionRatings,
|
|
|
+ productRatings
|
|
|
+ });
|
|
|
+
|
|
|
+ return {
|
|
|
+ submitted: true,
|
|
|
+ overallRating: averageRating,
|
|
|
+ totalFeedbacks: feedbacks.length,
|
|
|
+ averageRating,
|
|
|
+ dimensionRatings,
|
|
|
+ productRatings
|
|
|
+ };
|
|
|
+ } catch (error) {
|
|
|
+ console.error('❌ 获取评价统计失败:', error);
|
|
|
+ throw error;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 创建客户反馈
|
|
|
+ * @param feedbackData 反馈数据
|
|
|
+ * @returns 创建的反馈记录
|
|
|
+ */
|
|
|
+ async createFeedback(feedbackData: {
|
|
|
+ projectId: string;
|
|
|
+ contactId: string;
|
|
|
+ stage: string;
|
|
|
+ feedbackType: 'suggestion' | 'complaint' | 'praise';
|
|
|
+ content: string;
|
|
|
+ rating?: number;
|
|
|
+ productId?: string;
|
|
|
+ }): Promise<FmodeObject> {
|
|
|
+ try {
|
|
|
+ const ProjectFeedback = Parse.Object.extend('ProjectFeedback');
|
|
|
+ const feedback = new ProjectFeedback();
|
|
|
+
|
|
|
+ feedback.set('project', {
|
|
|
+ __type: 'Pointer',
|
|
|
+ className: 'Project',
|
|
|
+ objectId: feedbackData.projectId
|
|
|
+ });
|
|
|
+ feedback.set('contact', {
|
|
|
+ __type: 'Pointer',
|
|
|
+ className: 'ContactInfo',
|
|
|
+ objectId: feedbackData.contactId
|
|
|
+ });
|
|
|
+ feedback.set('stage', feedbackData.stage);
|
|
|
+ feedback.set('feedbackType', feedbackData.feedbackType);
|
|
|
+ feedback.set('content', feedbackData.content);
|
|
|
+ feedback.set('status', '待处理');
|
|
|
+
|
|
|
+ if (feedbackData.rating !== undefined) {
|
|
|
+ feedback.set('rating', feedbackData.rating);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (feedbackData.productId) {
|
|
|
+ feedback.set('product', {
|
|
|
+ __type: 'Pointer',
|
|
|
+ className: 'Product',
|
|
|
+ objectId: feedbackData.productId
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ feedback.set('isDeleted', false);
|
|
|
+
|
|
|
+ const savedFeedback = await feedback.save();
|
|
|
+ console.log(`✅ 创建客户反馈成功:`, savedFeedback.id);
|
|
|
+ return savedFeedback;
|
|
|
+ } catch (error) {
|
|
|
+ console.error('❌ 创建客户反馈失败:', error);
|
|
|
+ throw error;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * ==================== 项目数据相关 ====================
|
|
|
+ */
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取项目详情
|
|
|
+ * @param projectId 项目ID
|
|
|
+ * @returns 项目对象
|
|
|
+ */
|
|
|
+ async getProject(projectId: string): Promise<FmodeObject | null> {
|
|
|
+ try {
|
|
|
+ const query = new Parse.Query('Project');
|
|
|
+ query.include('contact');
|
|
|
+ query.include('assignee');
|
|
|
+ query.include('company');
|
|
|
+ const project = await query.get(projectId);
|
|
|
+ console.log(`✅ 获取项目详情成功: ${projectId}`);
|
|
|
+ return project;
|
|
|
+ } catch (error) {
|
|
|
+ console.error('❌ 获取项目详情失败:', error);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取项目的所有产品
|
|
|
+ * @param projectId 项目ID
|
|
|
+ * @returns 产品列表
|
|
|
+ */
|
|
|
+ async getProjectProducts(projectId: string): Promise<FmodeObject[]> {
|
|
|
+ try {
|
|
|
+ const query = new Parse.Query('Product');
|
|
|
+ query.equalTo('project', {
|
|
|
+ __type: 'Pointer',
|
|
|
+ className: 'Project',
|
|
|
+ objectId: projectId
|
|
|
+ });
|
|
|
+ query.notEqualTo('isDeleted', true);
|
|
|
+ query.include('profile'); // 包含设计师信息
|
|
|
+ query.descending('createdAt');
|
|
|
+
|
|
|
+ const products = await query.find();
|
|
|
+ console.log(`✅ 获取项目 ${projectId} 的产品,共 ${products.length} 个`);
|
|
|
+ return products;
|
|
|
+ } catch (error) {
|
|
|
+ console.error('❌ 获取项目产品失败:', error);
|
|
|
+ throw error;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 更新项目归档状态
|
|
|
+ * @param projectId 项目ID
|
|
|
+ * @param archived 是否归档
|
|
|
+ * @param archivedById 归档人ID
|
|
|
+ * @returns 更新后的项目
|
|
|
+ */
|
|
|
+ async updateProjectArchiveStatus(
|
|
|
+ projectId: string,
|
|
|
+ archived: boolean,
|
|
|
+ archivedById: string
|
|
|
+ ): Promise<FmodeObject | null> {
|
|
|
+ try {
|
|
|
+ const query = new Parse.Query('Project');
|
|
|
+ const project = await query.get(projectId);
|
|
|
+
|
|
|
+ const data = project.get('data') || {};
|
|
|
+ data.archiveStatus = {
|
|
|
+ archived,
|
|
|
+ archiveTime: archived ? new Date() : null,
|
|
|
+ archivedBy: archived ? {
|
|
|
+ __type: 'Pointer',
|
|
|
+ className: 'Profile',
|
|
|
+ objectId: archivedById
|
|
|
+ } : null
|
|
|
+ };
|
|
|
+
|
|
|
+ project.set('data', data);
|
|
|
+
|
|
|
+ if (archived) {
|
|
|
+ project.set('status', '已归档');
|
|
|
+ }
|
|
|
+
|
|
|
+ const savedProject = await project.save();
|
|
|
+ console.log(`✅ 更新项目归档状态成功: ${projectId} -> ${archived ? '已归档' : '取消归档'}`);
|
|
|
+ return savedProject;
|
|
|
+ } catch (error) {
|
|
|
+ console.error('❌ 更新项目归档状态失败:', error);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * ==================== 工具方法 ====================
|
|
|
+ */
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 格式化金额(添加千位分隔符)
|
|
|
+ * @param amount 金额
|
|
|
+ * @returns 格式化后的金额字符串
|
|
|
+ */
|
|
|
+ formatAmount(amount: number): string {
|
|
|
+ return amount.toLocaleString('zh-CN', {
|
|
|
+ minimumFractionDigits: 2,
|
|
|
+ maximumFractionDigits: 2
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算逾期天数
|
|
|
+ * @param dueDate 到期日期
|
|
|
+ * @returns 逾期天数(正数表示逾期,负数表示未到期)
|
|
|
+ */
|
|
|
+ calculateOverdueDays(dueDate: Date): number {
|
|
|
+ const now = new Date();
|
|
|
+ const diffTime = now.getTime() - dueDate.getTime();
|
|
|
+ const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
|
+ return diffDays;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取付款状态的中文描述
|
|
|
+ * @param status 状态
|
|
|
+ * @returns 中文描述
|
|
|
+ */
|
|
|
+ getPaymentStatusText(status: string): string {
|
|
|
+ const statusMap: Record<string, string> = {
|
|
|
+ 'pending': '待付款',
|
|
|
+ 'paid': '已付款',
|
|
|
+ 'overdue': '已逾期',
|
|
|
+ 'cancelled': '已取消',
|
|
|
+ 'partial': '部分付款',
|
|
|
+ 'completed': '已完成'
|
|
|
+ };
|
|
|
+ return statusMap[status] || status;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取付款类型的中文描述
|
|
|
+ * @param type 类型
|
|
|
+ * @returns 中文描述
|
|
|
+ */
|
|
|
+ getPaymentTypeText(type: string): string {
|
|
|
+ const typeMap: Record<string, string> = {
|
|
|
+ 'advance': '预付款',
|
|
|
+ 'milestone': '阶段款',
|
|
|
+ 'final': '尾款',
|
|
|
+ 'refund': '退款'
|
|
|
+ };
|
|
|
+ return typeMap[type] || type;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * ==================== 支付凭证AI分析相关 ====================
|
|
|
+ */
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 上传支付凭证并使用AI分析
|
|
|
+ * @param projectId 项目ID
|
|
|
+ * @param file 文件对象
|
|
|
+ * @param onProgress 上传和分析进度回调
|
|
|
+ * @returns 包含AI分析结果的ProjectFile对象
|
|
|
+ */
|
|
|
+ async uploadAndAnalyzeVoucher(
|
|
|
+ projectId: string,
|
|
|
+ file: File,
|
|
|
+ onProgress?: (progress: string) => void
|
|
|
+ ): Promise<{
|
|
|
+ projectFile: FmodeObject;
|
|
|
+ aiResult: Awaited<ReturnType<typeof this.paymentVoucherAI.analyzeVoucher>>;
|
|
|
+ }> {
|
|
|
+ try {
|
|
|
+ onProgress?.('正在上传支付凭证...');
|
|
|
+
|
|
|
+ // 1. 上传文件到ProjectFile
|
|
|
+ const projectFile = await this.projectFileService.uploadProjectFileWithRecord(
|
|
|
+ file,
|
|
|
+ projectId,
|
|
|
+ 'payment_voucher',
|
|
|
+ undefined, // spaceId
|
|
|
+ 'aftercare', // stage
|
|
|
+ { category: 'payment', description: '支付凭证' }
|
|
|
+ );
|
|
|
+
|
|
|
+ console.log('✅ 支付凭证上传成功:', projectFile.id);
|
|
|
+
|
|
|
+ onProgress?.('正在使用AI分析凭证...');
|
|
|
+
|
|
|
+ // 2. 使用AI分析支付凭证
|
|
|
+ const imageUrl = projectFile.get('url');
|
|
|
+ const aiResult = await this.paymentVoucherAI.analyzeVoucher({
|
|
|
+ imageUrl,
|
|
|
+ onProgress: (progress) => {
|
|
|
+ onProgress?.(`AI分析: ${progress}`);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ console.log('✅ AI分析完成:', aiResult);
|
|
|
+
|
|
|
+ // 3. 将AI分析结果保存到ProjectFile的data字段
|
|
|
+ const data = projectFile.get('data') || {};
|
|
|
+ projectFile.set('data', {
|
|
|
+ ...data,
|
|
|
+ aiAnalysis: {
|
|
|
+ amount: aiResult.amount,
|
|
|
+ paymentMethod: aiResult.paymentMethod,
|
|
|
+ paymentTime: aiResult.paymentTime,
|
|
|
+ transactionId: aiResult.transactionId,
|
|
|
+ payer: aiResult.payer,
|
|
|
+ receiver: aiResult.receiver,
|
|
|
+ confidence: aiResult.confidence,
|
|
|
+ rawText: aiResult.rawText,
|
|
|
+ analyzedAt: new Date()
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ await projectFile.save();
|
|
|
+
|
|
|
+ onProgress?.('分析完成!');
|
|
|
+
|
|
|
+ return {
|
|
|
+ projectFile,
|
|
|
+ aiResult
|
|
|
+ };
|
|
|
+ } catch (error) {
|
|
|
+ console.error('❌ 上传并分析支付凭证失败:', error);
|
|
|
+ throw error;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 创建支付记录(使用AI分析结果)
|
|
|
+ * @param projectId 项目ID
|
|
|
+ * @param voucherFileId 支付凭证文件ID
|
|
|
+ * @param aiResult AI分析结果
|
|
|
+ * @param productId 产品ID(可选,多产品项目时使用)
|
|
|
+ * @returns 创建的支付记录
|
|
|
+ */
|
|
|
+ async createPaymentFromAI(
|
|
|
+ projectId: string,
|
|
|
+ voucherFileId: string,
|
|
|
+ aiResult: Awaited<ReturnType<typeof this.paymentVoucherAI.analyzeVoucher>>,
|
|
|
+ productId?: string
|
|
|
+ ): Promise<FmodeObject> {
|
|
|
+ try {
|
|
|
+ const ProjectPayment = Parse.Object.extend('ProjectPayment');
|
|
|
+ const payment = new ProjectPayment();
|
|
|
+
|
|
|
+ // 基本信息
|
|
|
+ payment.set('project', {
|
|
|
+ __type: 'Pointer',
|
|
|
+ className: 'Project',
|
|
|
+ objectId: projectId
|
|
|
+ });
|
|
|
+
|
|
|
+ payment.set('amount', aiResult.amount);
|
|
|
+ payment.set('type', 'final'); // 默认为尾款
|
|
|
+ payment.set('method', aiResult.paymentMethod);
|
|
|
+ payment.set('status', 'paid');
|
|
|
+
|
|
|
+ // AI识别的信息
|
|
|
+ if (aiResult.paymentTime) {
|
|
|
+ payment.set('paymentDate', aiResult.paymentTime);
|
|
|
+ }
|
|
|
+
|
|
|
+ payment.set('notes', `AI识别 - 置信度: ${(aiResult.confidence * 100).toFixed(0)}%`);
|
|
|
+
|
|
|
+ // 支付凭证文件
|
|
|
+ payment.set('voucherFile', {
|
|
|
+ __type: 'Pointer',
|
|
|
+ className: 'ProjectFile',
|
|
|
+ objectId: voucherFileId
|
|
|
+ });
|
|
|
+
|
|
|
+ // 产品关联(如有)
|
|
|
+ if (productId) {
|
|
|
+ payment.set('product', {
|
|
|
+ __type: 'Pointer',
|
|
|
+ className: 'Product',
|
|
|
+ objectId: productId
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 保存AI分析详情
|
|
|
+ payment.set('data', {
|
|
|
+ aiAnalysis: {
|
|
|
+ transactionId: aiResult.transactionId,
|
|
|
+ payer: aiResult.payer,
|
|
|
+ receiver: aiResult.receiver,
|
|
|
+ confidence: aiResult.confidence,
|
|
|
+ rawText: aiResult.rawText,
|
|
|
+ analyzedAt: new Date()
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ const savedPayment = await payment.save();
|
|
|
+ console.log('✅ 从AI分析结果创建支付记录成功:', savedPayment.id);
|
|
|
+
|
|
|
+ return savedPayment;
|
|
|
+ } catch (error) {
|
|
|
+ console.error('❌ 从AI分析结果创建支付记录失败:', error);
|
|
|
+ throw error;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 一键上传分析并创建支付记录
|
|
|
+ * @param projectId 项目ID
|
|
|
+ * @param file 文件对象
|
|
|
+ * @param productId 产品ID(可选)
|
|
|
+ * @param onProgress 进度回调
|
|
|
+ * @returns 创建的支付记录
|
|
|
+ */
|
|
|
+ async uploadAnalyzeAndCreatePayment(
|
|
|
+ projectId: string,
|
|
|
+ file: File,
|
|
|
+ productId?: string,
|
|
|
+ onProgress?: (progress: string) => void
|
|
|
+ ): Promise<{
|
|
|
+ payment: FmodeObject;
|
|
|
+ aiResult: Awaited<ReturnType<typeof this.paymentVoucherAI.analyzeVoucher>>;
|
|
|
+ }> {
|
|
|
+ try {
|
|
|
+ // 1. 上传并分析
|
|
|
+ const { projectFile, aiResult } = await this.uploadAndAnalyzeVoucher(
|
|
|
+ projectId,
|
|
|
+ file,
|
|
|
+ onProgress
|
|
|
+ );
|
|
|
+
|
|
|
+ // 2. 验证AI结果
|
|
|
+ const validation = this.paymentVoucherAI.validateResult(aiResult);
|
|
|
+ if (!validation.valid) {
|
|
|
+ console.warn('⚠️ AI分析结果验证失败:', validation.errors);
|
|
|
+ onProgress?.('AI分析结果需要人工确认');
|
|
|
+ }
|
|
|
+
|
|
|
+ onProgress?.('正在创建支付记录...');
|
|
|
+
|
|
|
+ // 3. 创建支付记录
|
|
|
+ const payment = await this.createPaymentFromAI(
|
|
|
+ projectId,
|
|
|
+ projectFile.id,
|
|
|
+ aiResult,
|
|
|
+ productId
|
|
|
+ );
|
|
|
+
|
|
|
+ onProgress?.('完成!');
|
|
|
+
|
|
|
+ return {
|
|
|
+ payment,
|
|
|
+ aiResult
|
|
|
+ };
|
|
|
+ } catch (error) {
|
|
|
+ console.error('❌ 一键上传分析并创建支付记录失败:', error);
|
|
|
+ throw error;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|