feat: 新增多模块功能与服务实现
新增广告计划、用户资产、B2B交易、合规规则等核心模型 实现爬虫工作器、贸易服务、现金流预测等业务服务 添加RBAC权限测试、压力测试等测试用例 完善扩展程序的消息处理与内容脚本功能 重构应用入口与文档生成器 更新项目规则与业务闭环分析文档
This commit is contained in:
673
server/src/services/AfterSalesService.ts
Normal file
673
server/src/services/AfterSalesService.ts
Normal file
@@ -0,0 +1,673 @@
|
||||
import db from '../config/database';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
export interface ReturnRequest {
|
||||
orderId: string;
|
||||
reason: string;
|
||||
items: ReturnItem[];
|
||||
tenantId: string;
|
||||
shopId: string;
|
||||
taskId: string;
|
||||
traceId: string;
|
||||
businessType: 'TOC' | 'TOB';
|
||||
}
|
||||
|
||||
export interface ReturnItem {
|
||||
productId: string;
|
||||
skuId: string;
|
||||
quantity: number;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export interface ReturnRecord {
|
||||
id: string;
|
||||
orderId: string;
|
||||
status: ReturnStatus;
|
||||
reason: string;
|
||||
items: ReturnItem[];
|
||||
totalRefundAmount: number;
|
||||
tenantId: string;
|
||||
shopId: string;
|
||||
taskId: string;
|
||||
traceId: string;
|
||||
businessType: 'TOC' | 'TOB';
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export type ReturnStatus =
|
||||
| 'PENDING'
|
||||
| 'APPROVED'
|
||||
| 'REJECTED'
|
||||
| 'RETURNED'
|
||||
| 'REFUNDED'
|
||||
| 'CLOSED';
|
||||
|
||||
export interface RefundRequest {
|
||||
returnId: string;
|
||||
approvalResult: 'APPROVED' | 'REJECTED';
|
||||
approvedAmount?: number;
|
||||
reason?: string;
|
||||
tenantId: string;
|
||||
shopId: string;
|
||||
taskId: string;
|
||||
traceId: string;
|
||||
businessType: 'TOC' | 'TOB';
|
||||
}
|
||||
|
||||
export interface RefundRecord {
|
||||
id: string;
|
||||
returnId: string;
|
||||
orderId: string;
|
||||
amount: number;
|
||||
status: RefundStatus;
|
||||
method: string;
|
||||
tenantId: string;
|
||||
shopId: string;
|
||||
taskId: string;
|
||||
traceId: string;
|
||||
businessType: 'TOC' | 'TOB';
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export type RefundStatus =
|
||||
| 'PENDING_REVIEW'
|
||||
| 'APPROVED'
|
||||
| 'PROCESSING'
|
||||
| 'COMPLETED'
|
||||
| 'FAILED'
|
||||
| 'CANCELLED';
|
||||
|
||||
export interface ServiceTicket {
|
||||
id: string;
|
||||
returnId: string;
|
||||
type: TicketType;
|
||||
priority: TicketPriority;
|
||||
status: TicketStatus;
|
||||
subject: string;
|
||||
description: string;
|
||||
messages: TicketMessage[];
|
||||
tenantId: string;
|
||||
shopId: string;
|
||||
taskId: string;
|
||||
traceId: string;
|
||||
businessType: 'TOC' | 'TOB';
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export type TicketType =
|
||||
| 'RETURN'
|
||||
| 'REFUND'
|
||||
| 'EXCHANGE'
|
||||
| 'COMPLAINT'
|
||||
| 'INQUIRY';
|
||||
|
||||
export type TicketPriority =
|
||||
| 'LOW'
|
||||
| 'MEDIUM'
|
||||
| 'HIGH'
|
||||
| 'URGENT';
|
||||
|
||||
export type TicketStatus =
|
||||
| 'OPEN'
|
||||
| 'IN_PROGRESS'
|
||||
| 'WAITING_CUSTOMER'
|
||||
| 'WAITING_INTERNAL'
|
||||
| 'RESOLVED'
|
||||
| 'CLOSED';
|
||||
|
||||
export interface TicketMessage {
|
||||
id: string;
|
||||
sender: 'CUSTOMER' | 'AGENT' | 'SYSTEM';
|
||||
content: string;
|
||||
attachments?: string[];
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export class AfterSalesService {
|
||||
static async createReturnRequest(input: ReturnRequest): Promise<ReturnRecord> {
|
||||
const { orderId, reason, items, tenantId, shopId, taskId, traceId, businessType } = input;
|
||||
|
||||
logger.info('[AfterSalesService] Creating return request', {
|
||||
orderId,
|
||||
tenantId,
|
||||
shopId,
|
||||
taskId,
|
||||
traceId,
|
||||
businessType,
|
||||
itemCount: items.length,
|
||||
});
|
||||
|
||||
try {
|
||||
const order = await this.getOrderById(orderId, tenantId, traceId);
|
||||
if (!order) {
|
||||
throw new Error(`Order not found: ${orderId}`);
|
||||
}
|
||||
|
||||
if (order.status !== 'DELIVERED' && order.status !== 'COMPLETED') {
|
||||
throw new Error(`Order status ${order.status} does not allow return`);
|
||||
}
|
||||
|
||||
const returnDaysLimit = 30;
|
||||
const deliveredAt = order.deliveredAt || order.updatedAt;
|
||||
const daysSinceDelivery = Math.floor(
|
||||
(Date.now() - deliveredAt.getTime()) / (1000 * 60 * 60 * 24)
|
||||
);
|
||||
|
||||
if (daysSinceDelivery > returnDaysLimit) {
|
||||
throw new Error(`Return period expired (${daysSinceDelivery} days)`);
|
||||
}
|
||||
|
||||
const returnId = `RET-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
const totalRefundAmount = await this.calculateRefundAmount(
|
||||
order,
|
||||
items,
|
||||
tenantId,
|
||||
traceId
|
||||
);
|
||||
|
||||
const returnRecord: ReturnRecord = {
|
||||
id: returnId,
|
||||
orderId,
|
||||
status: 'PENDING',
|
||||
reason,
|
||||
items,
|
||||
totalRefundAmount,
|
||||
tenantId,
|
||||
shopId,
|
||||
taskId,
|
||||
traceId,
|
||||
businessType,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
await db('cf_return_requests').insert({
|
||||
id: returnRecord.id,
|
||||
order_id: returnRecord.orderId,
|
||||
status: returnRecord.status,
|
||||
reason: returnRecord.reason,
|
||||
items: JSON.stringify(returnRecord.items),
|
||||
total_refund_amount: returnRecord.totalRefundAmount,
|
||||
tenant_id: returnRecord.tenantId,
|
||||
shop_id: returnRecord.shopId,
|
||||
task_id: returnRecord.taskId,
|
||||
trace_id: returnRecord.traceId,
|
||||
business_type: returnRecord.businessType,
|
||||
created_at: returnRecord.createdAt,
|
||||
updated_at: returnRecord.updatedAt,
|
||||
});
|
||||
|
||||
await this.createServiceTicket({
|
||||
returnId,
|
||||
type: 'RETURN',
|
||||
priority: this.determinePriority(reason),
|
||||
subject: `Return Request - Order ${orderId}`,
|
||||
description: reason,
|
||||
tenantId,
|
||||
shopId,
|
||||
taskId,
|
||||
traceId,
|
||||
businessType,
|
||||
});
|
||||
|
||||
logger.info('[AfterSalesService] Return request created', {
|
||||
returnId,
|
||||
orderId,
|
||||
tenantId,
|
||||
traceId,
|
||||
status: returnRecord.status,
|
||||
});
|
||||
|
||||
return returnRecord;
|
||||
} catch (error: any) {
|
||||
logger.error('[AfterSalesService] Failed to create return request', {
|
||||
orderId,
|
||||
tenantId,
|
||||
traceId,
|
||||
error: error.message,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async processRefund(input: RefundRequest): Promise<RefundRecord> {
|
||||
const { returnId, approvalResult, approvedAmount, reason, tenantId, shopId, taskId, traceId, businessType } = input;
|
||||
|
||||
logger.info('[AfterSalesService] Processing refund', {
|
||||
returnId,
|
||||
approvalResult,
|
||||
tenantId,
|
||||
shopId,
|
||||
taskId,
|
||||
traceId,
|
||||
businessType,
|
||||
});
|
||||
|
||||
try {
|
||||
const returnRecord = await this.getReturnById(returnId, tenantId, traceId);
|
||||
if (!returnRecord) {
|
||||
throw new Error(`Return request not found: ${returnId}`);
|
||||
}
|
||||
|
||||
if (returnRecord.status !== 'PENDING' && returnRecord.status !== 'APPROVED') {
|
||||
throw new Error(`Return status ${returnRecord.status} cannot be refunded`);
|
||||
}
|
||||
|
||||
const refundId = `REF-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const refundAmount = approvedAmount || returnRecord.totalRefundAmount;
|
||||
|
||||
let refundStatus: RefundStatus;
|
||||
if (approvalResult === 'REJECTED') {
|
||||
refundStatus = 'CANCELLED';
|
||||
} else {
|
||||
refundStatus = 'PENDING_REVIEW';
|
||||
}
|
||||
|
||||
const refundRecord: RefundRecord = {
|
||||
id: refundId,
|
||||
returnId,
|
||||
orderId: returnRecord.orderId,
|
||||
amount: refundAmount,
|
||||
status: refundStatus,
|
||||
method: 'ORIGINAL_PAYMENT',
|
||||
tenantId,
|
||||
shopId,
|
||||
taskId,
|
||||
traceId,
|
||||
businessType,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
await db('cf_refunds').insert({
|
||||
id: refundRecord.id,
|
||||
return_id: refundRecord.returnId,
|
||||
order_id: refundRecord.orderId,
|
||||
amount: refundRecord.amount,
|
||||
status: refundRecord.status,
|
||||
method: refundRecord.method,
|
||||
tenant_id: refundRecord.tenantId,
|
||||
shop_id: refundRecord.shopId,
|
||||
task_id: refundRecord.taskId,
|
||||
trace_id: refundRecord.traceId,
|
||||
business_type: refundRecord.businessType,
|
||||
created_at: refundRecord.createdAt,
|
||||
updated_at: refundRecord.updatedAt,
|
||||
});
|
||||
|
||||
if (approvalResult === 'APPROVED') {
|
||||
await db('cf_return_requests')
|
||||
.where({ id: returnId, tenant_id: tenantId })
|
||||
.update({ status: 'APPROVED', updated_at: new Date() });
|
||||
|
||||
await this.executeRefundWorkflow(refundRecord, tenantId, traceId);
|
||||
} else {
|
||||
await db('cf_return_requests')
|
||||
.where({ id: returnId, tenant_id: tenantId })
|
||||
.update({ status: 'REJECTED', updated_at: new Date() });
|
||||
}
|
||||
|
||||
logger.info('[AfterSalesService] Refund processed', {
|
||||
refundId,
|
||||
returnId,
|
||||
status: refundRecord.status,
|
||||
tenantId,
|
||||
traceId,
|
||||
});
|
||||
|
||||
return refundRecord;
|
||||
} catch (error: any) {
|
||||
logger.error('[AfterSalesService] Failed to process refund', {
|
||||
returnId,
|
||||
tenantId,
|
||||
traceId,
|
||||
error: error.message,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async createServiceTicket(input: {
|
||||
returnId: string;
|
||||
type: TicketType;
|
||||
priority: TicketPriority;
|
||||
subject: string;
|
||||
description: string;
|
||||
tenantId: string;
|
||||
shopId: string;
|
||||
taskId: string;
|
||||
traceId: string;
|
||||
businessType: 'TOC' | 'TOB';
|
||||
}): Promise<ServiceTicket> {
|
||||
const { returnId, type, priority, subject, description, tenantId, shopId, taskId, traceId, businessType } = input;
|
||||
|
||||
logger.info('[AfterSalesService] Creating service ticket', {
|
||||
returnId,
|
||||
type,
|
||||
priority,
|
||||
tenantId,
|
||||
shopId,
|
||||
taskId,
|
||||
traceId,
|
||||
businessType,
|
||||
});
|
||||
|
||||
try {
|
||||
const ticketId = `TKT-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
const initialMessage: TicketMessage = {
|
||||
id: `MSG-${Date.now()}`,
|
||||
sender: 'SYSTEM',
|
||||
content: `Ticket created: ${subject}`,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
const ticket: ServiceTicket = {
|
||||
id: ticketId,
|
||||
returnId,
|
||||
type,
|
||||
priority,
|
||||
status: 'OPEN',
|
||||
subject,
|
||||
description,
|
||||
messages: [initialMessage],
|
||||
tenantId,
|
||||
shopId,
|
||||
taskId,
|
||||
traceId,
|
||||
businessType,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
await db('cf_service_tickets').insert({
|
||||
id: ticket.id,
|
||||
return_id: ticket.returnId,
|
||||
type: ticket.type,
|
||||
priority: ticket.priority,
|
||||
status: ticket.status,
|
||||
subject: ticket.subject,
|
||||
description: ticket.description,
|
||||
messages: JSON.stringify(ticket.messages),
|
||||
tenant_id: ticket.tenantId,
|
||||
shop_id: ticket.shopId,
|
||||
task_id: ticket.taskId,
|
||||
trace_id: ticket.traceId,
|
||||
business_type: ticket.businessType,
|
||||
created_at: ticket.createdAt,
|
||||
updated_at: ticket.updatedAt,
|
||||
});
|
||||
|
||||
logger.info('[AfterSalesService] Service ticket created', {
|
||||
ticketId,
|
||||
returnId,
|
||||
type,
|
||||
priority,
|
||||
tenantId,
|
||||
traceId,
|
||||
});
|
||||
|
||||
return ticket;
|
||||
} catch (error: any) {
|
||||
logger.error('[AfterSalesService] Failed to create service ticket', {
|
||||
returnId,
|
||||
tenantId,
|
||||
traceId,
|
||||
error: error.message,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async addTicketMessage(
|
||||
ticketId: string,
|
||||
sender: 'CUSTOMER' | 'AGENT' | 'SYSTEM',
|
||||
content: string,
|
||||
options: {
|
||||
tenantId: string;
|
||||
traceId: string;
|
||||
attachments?: string[];
|
||||
}
|
||||
): Promise<ServiceTicket> {
|
||||
const { tenantId, traceId, attachments } = options;
|
||||
|
||||
logger.info('[AfterSalesService] Adding ticket message', {
|
||||
ticketId,
|
||||
sender,
|
||||
tenantId,
|
||||
traceId,
|
||||
});
|
||||
|
||||
try {
|
||||
const ticket = await db('cf_service_tickets')
|
||||
.where({ id: ticketId, tenant_id: tenantId })
|
||||
.first();
|
||||
|
||||
if (!ticket) {
|
||||
throw new Error(`Ticket not found: ${ticketId}`);
|
||||
}
|
||||
|
||||
const messages: TicketMessage[] = JSON.parse(ticket.messages || '[]');
|
||||
const newMessage: TicketMessage = {
|
||||
id: `MSG-${Date.now()}`,
|
||||
sender,
|
||||
content,
|
||||
attachments,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
messages.push(newMessage);
|
||||
|
||||
await db('cf_service_tickets')
|
||||
.where({ id: ticketId, tenant_id: tenantId })
|
||||
.update({
|
||||
messages: JSON.stringify(messages),
|
||||
status: sender === 'CUSTOMER' ? 'IN_PROGRESS' : 'WAITING_CUSTOMER',
|
||||
updated_at: new Date(),
|
||||
});
|
||||
|
||||
logger.info('[AfterSalesService] Ticket message added', {
|
||||
ticketId,
|
||||
sender,
|
||||
tenantId,
|
||||
traceId,
|
||||
});
|
||||
|
||||
return {
|
||||
...ticket,
|
||||
messages,
|
||||
} as ServiceTicket;
|
||||
} catch (error: any) {
|
||||
logger.error('[AfterSalesService] Failed to add ticket message', {
|
||||
ticketId,
|
||||
tenantId,
|
||||
traceId,
|
||||
error: error.message,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async getReturnById(returnId: string, tenantId: string, traceId: string): Promise<ReturnRecord | null> {
|
||||
const record = await db('cf_return_requests')
|
||||
.where({ id: returnId, tenant_id: tenantId })
|
||||
.first();
|
||||
|
||||
if (!record) return null;
|
||||
|
||||
return {
|
||||
id: record.id,
|
||||
orderId: record.order_id,
|
||||
status: record.status,
|
||||
reason: record.reason,
|
||||
items: JSON.parse(record.items || '[]'),
|
||||
totalRefundAmount: Number(record.total_refund_amount),
|
||||
tenantId: record.tenant_id,
|
||||
shopId: record.shop_id,
|
||||
taskId: record.task_id,
|
||||
traceId: record.trace_id,
|
||||
businessType: record.business_type,
|
||||
createdAt: record.created_at,
|
||||
updatedAt: record.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
private static async getOrderById(orderId: string, tenantId: string, traceId: string): Promise<any> {
|
||||
const order = await db('cf_orders')
|
||||
.where({ id: orderId, tenant_id: tenantId })
|
||||
.first();
|
||||
|
||||
return order;
|
||||
}
|
||||
|
||||
private static async calculateRefundAmount(
|
||||
order: any,
|
||||
items: ReturnItem[],
|
||||
tenantId: string,
|
||||
traceId: string
|
||||
): Promise<number> {
|
||||
const orderItems = JSON.parse(order.items || '[]');
|
||||
let totalAmount = 0;
|
||||
|
||||
for (const returnItem of items) {
|
||||
const orderItem = orderItems.find((oi: any) =>
|
||||
oi.productId === returnItem.productId && oi.skuId === returnItem.skuId
|
||||
);
|
||||
|
||||
if (orderItem) {
|
||||
totalAmount += orderItem.unitPrice * returnItem.quantity;
|
||||
}
|
||||
}
|
||||
|
||||
return Number(totalAmount.toFixed(2));
|
||||
}
|
||||
|
||||
private static determinePriority(reason: string): TicketPriority {
|
||||
const highPriorityKeywords = ['损坏', '缺失', '错误', 'damaged', 'missing', 'wrong', 'defective'];
|
||||
const urgentKeywords = ['欺诈', '投诉', 'fraud', 'complaint', 'legal'];
|
||||
|
||||
const lowerReason = reason.toLowerCase();
|
||||
|
||||
if (urgentKeywords.some(kw => lowerReason.includes(kw))) {
|
||||
return 'URGENT';
|
||||
}
|
||||
if (highPriorityKeywords.some(kw => lowerReason.includes(kw))) {
|
||||
return 'HIGH';
|
||||
}
|
||||
return 'MEDIUM';
|
||||
}
|
||||
|
||||
private static async executeRefundWorkflow(
|
||||
refund: RefundRecord,
|
||||
tenantId: string,
|
||||
traceId: string
|
||||
): Promise<void> {
|
||||
logger.info('[AfterSalesService] Executing refund workflow', {
|
||||
refundId: refund.id,
|
||||
tenantId,
|
||||
traceId,
|
||||
});
|
||||
|
||||
await db('cf_refunds')
|
||||
.where({ id: refund.id, tenant_id: tenantId })
|
||||
.update({ status: 'PROCESSING', updated_at: new Date() });
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
await db('cf_refunds')
|
||||
.where({ id: refund.id, tenant_id: tenantId })
|
||||
.update({ status: 'COMPLETED', updated_at: new Date() });
|
||||
|
||||
await db('cf_return_requests')
|
||||
.where({ id: refund.returnId, tenant_id: tenantId })
|
||||
.update({ status: 'REFUNDED', updated_at: new Date() });
|
||||
|
||||
logger.info('[AfterSalesService] Refund workflow completed', {
|
||||
refundId: refund.id,
|
||||
tenantId,
|
||||
traceId,
|
||||
});
|
||||
}
|
||||
|
||||
static async initializeTables(): Promise<void> {
|
||||
logger.info('[AfterSalesService] Initializing after-sales tables');
|
||||
|
||||
try {
|
||||
if (!(await db.schema.hasTable('cf_return_requests'))) {
|
||||
await db.schema.createTable('cf_return_requests', table => {
|
||||
table.string('id').primary();
|
||||
table.string('order_id').notNullable();
|
||||
table.enum('status', ['PENDING', 'APPROVED', 'REJECTED', 'RETURNED', 'REFUNDED', 'CLOSED']).defaultTo('PENDING');
|
||||
table.text('reason');
|
||||
table.json('items');
|
||||
table.decimal('total_refund_amount', 12, 2).defaultTo(0);
|
||||
table.string('tenant_id').notNullable();
|
||||
table.string('shop_id');
|
||||
table.string('task_id');
|
||||
table.string('trace_id');
|
||||
table.enum('business_type', ['TOC', 'TOB']).defaultTo('TOC');
|
||||
table.timestamp('created_at').defaultTo(db.fn.now());
|
||||
table.timestamp('updated_at').defaultTo(db.fn.now());
|
||||
table.index(['order_id', 'tenant_id']);
|
||||
table.index(['status', 'tenant_id']);
|
||||
});
|
||||
logger.info('[AfterSalesService] Table cf_return_requests created');
|
||||
}
|
||||
|
||||
if (!(await db.schema.hasTable('cf_refunds'))) {
|
||||
await db.schema.createTable('cf_refunds', table => {
|
||||
table.string('id').primary();
|
||||
table.string('return_id').notNullable();
|
||||
table.string('order_id').notNullable();
|
||||
table.decimal('amount', 12, 2).notNullable();
|
||||
table.enum('status', ['PENDING_REVIEW', 'APPROVED', 'PROCESSING', 'COMPLETED', 'FAILED', 'CANCELLED']).defaultTo('PENDING_REVIEW');
|
||||
table.string('method').defaultTo('ORIGINAL_PAYMENT');
|
||||
table.string('tenant_id').notNullable();
|
||||
table.string('shop_id');
|
||||
table.string('task_id');
|
||||
table.string('trace_id');
|
||||
table.enum('business_type', ['TOC', 'TOB']).defaultTo('TOC');
|
||||
table.timestamp('created_at').defaultTo(db.fn.now());
|
||||
table.timestamp('updated_at').defaultTo(db.fn.now());
|
||||
table.index(['return_id', 'tenant_id']);
|
||||
table.index(['order_id', 'tenant_id']);
|
||||
table.index(['status', 'tenant_id']);
|
||||
});
|
||||
logger.info('[AfterSalesService] Table cf_refunds created');
|
||||
}
|
||||
|
||||
if (!(await db.schema.hasTable('cf_service_tickets'))) {
|
||||
await db.schema.createTable('cf_service_tickets', table => {
|
||||
table.string('id').primary();
|
||||
table.string('return_id');
|
||||
table.enum('type', ['RETURN', 'REFUND', 'EXCHANGE', 'COMPLAINT', 'INQUIRY']).defaultTo('INQUIRY');
|
||||
table.enum('priority', ['LOW', 'MEDIUM', 'HIGH', 'URGENT']).defaultTo('MEDIUM');
|
||||
table.enum('status', ['OPEN', 'IN_PROGRESS', 'WAITING_CUSTOMER', 'WAITING_INTERNAL', 'RESOLVED', 'CLOSED']).defaultTo('OPEN');
|
||||
table.string('subject');
|
||||
table.text('description');
|
||||
table.json('messages');
|
||||
table.string('tenant_id').notNullable();
|
||||
table.string('shop_id');
|
||||
table.string('task_id');
|
||||
table.string('trace_id');
|
||||
table.enum('business_type', ['TOC', 'TOB']).defaultTo('TOC');
|
||||
table.timestamp('created_at').defaultTo(db.fn.now());
|
||||
table.timestamp('updated_at').defaultTo(db.fn.now());
|
||||
table.index(['return_id', 'tenant_id']);
|
||||
table.index(['status', 'tenant_id']);
|
||||
table.index(['priority', 'tenant_id']);
|
||||
});
|
||||
logger.info('[AfterSalesService] Table cf_service_tickets created');
|
||||
}
|
||||
|
||||
logger.info('[AfterSalesService] After-sales tables initialized successfully');
|
||||
} catch (error: any) {
|
||||
logger.error(`[AfterSalesService] Failed to initialize after-sales tables - error: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user