2026-03-17 22:07:19 +08:00
|
|
|
|
import { Request, Response } from 'express';
|
|
|
|
|
|
import { logger } from '../../utils/logger';
|
|
|
|
|
|
import { ConsumerOrderService, ConsumerOrder } from '../../domains/Trade/ConsumerOrderService';
|
|
|
|
|
|
import { OrderService } from '../../services/OrderService';
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* [BIZ_OPS_01] 多平台订单 Webhook 接收器 (Order Webhook Receiver)
|
|
|
|
|
|
* @description 接收来自不同平台 (Amazon, AliExpress, Shopify, TikTok) 的订单 Webhook 推送,
|
|
|
|
|
|
* 并将其映射为系统统一的 cf_order 模型进行入库处理。
|
|
|
|
|
|
*/
|
|
|
|
|
|
export class OrderController {
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 平台 Webhook 入口
|
|
|
|
|
|
* POST /api/v1/orders/webhook/:platform
|
|
|
|
|
|
*/
|
|
|
|
|
|
static async handlePlatformWebhook(req: Request, res: Response) {
|
|
|
|
|
|
const { platform } = req.params;
|
|
|
|
|
|
const payload = req.body;
|
|
|
|
|
|
const tenantId = req.headers['x-tenant-id'] as string || 'default-tenant';
|
|
|
|
|
|
const shopId = req.headers['x-shop-id'] as string || 'default-shop';
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(`[OrderWebhook] Received webhook from platform: ${platform} for Tenant: ${tenantId}`);
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 1. 根据不同平台解析 Payload (Mapping)
|
2026-03-19 01:39:34 +08:00
|
|
|
|
const normalizedOrder = OrderService.mapPlatformPayloadToOrder(platform as string, payload, tenantId, shopId);
|
2026-03-17 22:07:19 +08:00
|
|
|
|
|
|
|
|
|
|
if (!normalizedOrder) {
|
|
|
|
|
|
return res.status(400).json({ success: false, error: `Unsupported or invalid payload for platform: ${platform}` });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 调用 ConsumerOrderService 进行同步
|
|
|
|
|
|
const orderId = await ConsumerOrderService.syncPlatformOrder(normalizedOrder);
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(`[OrderWebhook] Successfully synced order ${normalizedOrder.platform_order_id} to internal ID: ${orderId}`);
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 返回平台要求的 200 OK
|
|
|
|
|
|
return res.json({ success: true, orderId });
|
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
|
logger.error(`[OrderWebhook] Webhook handling failed for ${platform}: ${err.message}`);
|
|
|
|
|
|
return res.status(500).json({ success: false, error: 'Internal server error' });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 手动触发同步 (API 入口)
|
|
|
|
|
|
* POST /api/v1/orders/sync
|
|
|
|
|
|
*/
|
|
|
|
|
|
static async triggerManualSync(req: Request, res: Response) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { platform, shopId } = req.body;
|
|
|
|
|
|
const { tenantId } = (req as any).traceContext || { tenantId: 'default-tenant' };
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(`[OrderSync] Manual sync triggered for Platform: ${platform}, Shop: ${shopId}`);
|
|
|
|
|
|
|
|
|
|
|
|
// 此处应调用各平台 Connector 的 supportsOrderPull 能力
|
|
|
|
|
|
// 示例:Mock 拉取成功
|
|
|
|
|
|
res.json({ success: true, message: `Manual sync triggered for ${platform}` });
|
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
|
res.status(500).json({ success: false, error: err.message });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取订单统计
|
|
|
|
|
|
* GET /api/v1/orders/stats
|
|
|
|
|
|
*/
|
|
|
|
|
|
static async getStats(req: Request, res: Response) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { tenantId } = (req as any).traceContext || { tenantId: 'default-tenant' };
|
|
|
|
|
|
const stats = await ConsumerOrderService.getOrderStats(tenantId);
|
|
|
|
|
|
res.json({ success: true, data: stats });
|
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
|
res.status(500).json({ success: false, error: err.message });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 创建订单
|
|
|
|
|
|
* POST /api/v1/orders
|
|
|
|
|
|
*/
|
|
|
|
|
|
static async createOrder(req: Request, res: Response) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { tenantId } = (req as any).traceContext || { tenantId: 'default-tenant' };
|
|
|
|
|
|
const orderData = { ...req.body, tenant_id: tenantId };
|
|
|
|
|
|
const orderId = await OrderService.createOrder(orderData);
|
|
|
|
|
|
res.json({ success: true, orderId });
|
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
|
res.status(500).json({ success: false, error: err.message });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取订单详情
|
|
|
|
|
|
* GET /api/v1/orders/:id
|
|
|
|
|
|
*/
|
|
|
|
|
|
static async getOrderById(req: Request, res: Response) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { id } = req.params;
|
|
|
|
|
|
const { tenantId } = (req as any).traceContext || { tenantId: 'default-tenant' };
|
2026-03-18 13:38:05 +08:00
|
|
|
|
const order = await OrderService.getOrderById(id as string, tenantId);
|
2026-03-17 22:07:19 +08:00
|
|
|
|
if (order) {
|
|
|
|
|
|
res.json({ success: true, data: order });
|
|
|
|
|
|
} else {
|
|
|
|
|
|
res.status(404).json({ success: false, error: 'Order not found' });
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
|
res.status(500).json({ success: false, error: err.message });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 更新订单
|
|
|
|
|
|
* PUT /api/v1/orders/:id
|
|
|
|
|
|
*/
|
|
|
|
|
|
static async updateOrder(req: Request, res: Response) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { id } = req.params;
|
|
|
|
|
|
const { tenantId } = (req as any).traceContext || { tenantId: 'default-tenant' };
|
2026-03-18 13:38:05 +08:00
|
|
|
|
await OrderService.updateOrder(id as string, tenantId, req.body);
|
2026-03-17 22:07:19 +08:00
|
|
|
|
res.json({ success: true, message: 'Order updated successfully' });
|
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
|
res.status(500).json({ success: false, error: err.message });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 删除订单
|
|
|
|
|
|
* DELETE /api/v1/orders/:id
|
|
|
|
|
|
*/
|
|
|
|
|
|
static async deleteOrder(req: Request, res: Response) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { id } = req.params;
|
|
|
|
|
|
const { tenantId } = (req as any).traceContext || { tenantId: 'default-tenant' };
|
2026-03-18 13:38:05 +08:00
|
|
|
|
await OrderService.deleteOrder(id as string, tenantId);
|
2026-03-17 22:07:19 +08:00
|
|
|
|
res.json({ success: true, message: 'Order deleted successfully' });
|
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
|
res.status(500).json({ success: false, error: err.message });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取订单列表
|
|
|
|
|
|
* GET /api/v1/orders
|
|
|
|
|
|
*/
|
|
|
|
|
|
static async getOrders(req: Request, res: Response) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { tenantId } = (req as any).traceContext || { tenantId: 'default-tenant' };
|
|
|
|
|
|
const params = {
|
|
|
|
|
|
page: parseInt(req.query.page as string) || 1,
|
|
|
|
|
|
pageSize: parseInt(req.query.pageSize as string) || 20,
|
|
|
|
|
|
status: req.query.status as string,
|
|
|
|
|
|
platform: req.query.platform as string,
|
|
|
|
|
|
startDate: req.query.startDate as string,
|
|
|
|
|
|
endDate: req.query.endDate as string,
|
|
|
|
|
|
};
|
|
|
|
|
|
const result = await OrderService.getOrders(tenantId, params);
|
|
|
|
|
|
res.json({ success: true, data: result });
|
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
|
res.status(500).json({ success: false, error: err.message });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 批量更新订单
|
|
|
|
|
|
* PUT /api/v1/orders/batch
|
|
|
|
|
|
*/
|
|
|
|
|
|
static async batchUpdateOrders(req: Request, res: Response) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { tenantId } = (req as any).traceContext || { tenantId: 'default-tenant' };
|
|
|
|
|
|
const { orderIds, updates } = req.body;
|
|
|
|
|
|
const result = await OrderService.batchUpdateOrders(tenantId, orderIds, updates);
|
|
|
|
|
|
res.json({ success: true, data: result });
|
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
|
res.status(500).json({ success: false, error: err.message });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 订单状态流转
|
|
|
|
|
|
* POST /api/v1/orders/:id/status
|
|
|
|
|
|
*/
|
|
|
|
|
|
static async transitionOrderStatus(req: Request, res: Response) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { id } = req.params;
|
|
|
|
|
|
const { tenantId } = (req as any).traceContext || { tenantId: 'default-tenant' };
|
|
|
|
|
|
const { status, reason } = req.body;
|
2026-03-18 13:38:05 +08:00
|
|
|
|
await OrderService.transitionOrderStatus(tenantId, id as string, status, reason);
|
2026-03-17 22:07:19 +08:00
|
|
|
|
res.json({ success: true, message: 'Order status updated successfully' });
|
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
|
res.status(500).json({ success: false, error: err.message });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 批量审核订单
|
|
|
|
|
|
* POST /api/v1/orders/batch/audit
|
|
|
|
|
|
*/
|
|
|
|
|
|
static async batchAuditOrders(req: Request, res: Response) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { tenantId } = (req as any).traceContext || { tenantId: 'default-tenant' };
|
|
|
|
|
|
const { orderIds } = req.body;
|
|
|
|
|
|
const result = await OrderService.batchAuditOrders(tenantId, orderIds);
|
|
|
|
|
|
res.json({ success: true, data: result });
|
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
|
res.status(500).json({ success: false, error: err.message });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 批量发货
|
|
|
|
|
|
* POST /api/v1/orders/batch/ship
|
|
|
|
|
|
*/
|
|
|
|
|
|
static async batchShipOrders(req: Request, res: Response) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { tenantId } = (req as any).traceContext || { tenantId: 'default-tenant' };
|
|
|
|
|
|
const { orderIds } = req.body;
|
|
|
|
|
|
const result = await OrderService.batchShipOrders(tenantId, orderIds);
|
|
|
|
|
|
res.json({ success: true, data: result });
|
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
|
res.status(500).json({ success: false, error: err.message });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 标记订单为异常
|
|
|
|
|
|
* POST /api/v1/orders/:id/exception
|
|
|
|
|
|
*/
|
|
|
|
|
|
static async markOrderAsException(req: Request, res: Response) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { id } = req.params;
|
|
|
|
|
|
const { tenantId } = (req as any).traceContext || { tenantId: 'default-tenant' };
|
|
|
|
|
|
const { reason } = req.body;
|
2026-03-18 13:38:05 +08:00
|
|
|
|
await OrderService.markOrderAsException(tenantId, id as string, reason);
|
2026-03-17 22:07:19 +08:00
|
|
|
|
res.json({ success: true, message: 'Order marked as exception' });
|
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
|
res.status(500).json({ success: false, error: err.message });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 自动改派订单
|
|
|
|
|
|
* POST /api/v1/orders/:id/reroute
|
|
|
|
|
|
*/
|
|
|
|
|
|
static async autoRerouteOrder(req: Request, res: Response) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { id } = req.params;
|
|
|
|
|
|
const { tenantId } = (req as any).traceContext || { tenantId: 'default-tenant' };
|
2026-03-18 13:38:05 +08:00
|
|
|
|
await OrderService.autoRerouteOrder(tenantId, id as string);
|
2026-03-17 22:07:19 +08:00
|
|
|
|
res.json({ success: true, message: 'Order rerouted successfully' });
|
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
|
res.status(500).json({ success: false, error: err.message });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 重试异常订单
|
|
|
|
|
|
* POST /api/v1/orders/:id/retry
|
|
|
|
|
|
*/
|
|
|
|
|
|
static async retryExceptionOrder(req: Request, res: Response) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { id } = req.params;
|
|
|
|
|
|
const { tenantId } = (req as any).traceContext || { tenantId: 'default-tenant' };
|
2026-03-18 13:38:05 +08:00
|
|
|
|
await OrderService.retryExceptionOrder(tenantId, id as string);
|
2026-03-17 22:07:19 +08:00
|
|
|
|
res.json({ success: true, message: 'Order retried successfully' });
|
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
|
res.status(500).json({ success: false, error: err.message });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 取消订单
|
|
|
|
|
|
* POST /api/v1/orders/:id/cancel
|
|
|
|
|
|
*/
|
|
|
|
|
|
static async cancelOrder(req: Request, res: Response) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { id } = req.params;
|
|
|
|
|
|
const { tenantId } = (req as any).traceContext || { tenantId: 'default-tenant' };
|
|
|
|
|
|
const { reason } = req.body;
|
2026-03-18 13:38:05 +08:00
|
|
|
|
await OrderService.cancelOrder(tenantId, id as string, reason);
|
2026-03-17 22:07:19 +08:00
|
|
|
|
res.json({ success: true, message: 'Order cancelled successfully' });
|
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
|
res.status(500).json({ success: false, error: err.message });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 申请退款
|
|
|
|
|
|
* POST /api/v1/orders/:id/refund
|
|
|
|
|
|
*/
|
|
|
|
|
|
static async requestRefund(req: Request, res: Response) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { id } = req.params;
|
|
|
|
|
|
const { tenantId } = (req as any).traceContext || { tenantId: 'default-tenant' };
|
|
|
|
|
|
const { reason, amount } = req.body;
|
2026-03-18 13:38:05 +08:00
|
|
|
|
const refundId = await OrderService.requestRefund(tenantId, id as string, reason, amount);
|
2026-03-17 22:07:19 +08:00
|
|
|
|
res.json({ success: true, refundId });
|
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
|
res.status(500).json({ success: false, error: err.message });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 审批退款
|
|
|
|
|
|
* POST /api/v1/orders/refund/:id/approve
|
|
|
|
|
|
*/
|
|
|
|
|
|
static async approveRefund(req: Request, res: Response) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { id } = req.params;
|
|
|
|
|
|
const { tenantId } = (req as any).traceContext || { tenantId: 'default-tenant' };
|
|
|
|
|
|
const { approved, note } = req.body;
|
2026-03-18 13:38:05 +08:00
|
|
|
|
await OrderService.approveRefund(tenantId, id as string, approved, note);
|
2026-03-17 22:07:19 +08:00
|
|
|
|
res.json({ success: true, message: 'Refund processed successfully' });
|
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
|
res.status(500).json({ success: false, error: err.message });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 申请售后
|
|
|
|
|
|
* POST /api/v1/orders/:id/after-sales
|
|
|
|
|
|
*/
|
|
|
|
|
|
static async requestAfterSales(req: Request, res: Response) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { id } = req.params;
|
|
|
|
|
|
const { tenantId } = (req as any).traceContext || { tenantId: 'default-tenant' };
|
|
|
|
|
|
const { type, reason, items } = req.body;
|
2026-03-18 13:38:05 +08:00
|
|
|
|
const serviceId = await OrderService.requestAfterSales(tenantId, id as string, type, reason, items);
|
2026-03-17 22:07:19 +08:00
|
|
|
|
res.json({ success: true, serviceId });
|
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
|
res.status(500).json({ success: false, error: err.message });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 处理售后申请
|
|
|
|
|
|
* POST /api/v1/orders/after-sales/:id/process
|
|
|
|
|
|
*/
|
|
|
|
|
|
static async processAfterSales(req: Request, res: Response) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { id } = req.params;
|
|
|
|
|
|
const { tenantId } = (req as any).traceContext || { tenantId: 'default-tenant' };
|
|
|
|
|
|
const { action, note } = req.body;
|
2026-03-18 13:38:05 +08:00
|
|
|
|
await OrderService.processAfterSales(tenantId, id as string, action, note);
|
2026-03-17 22:07:19 +08:00
|
|
|
|
res.json({ success: true, message: 'After-sales service processed successfully' });
|
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
|
res.status(500).json({ success: false, error: err.message });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 完成订单
|
|
|
|
|
|
* POST /api/v1/orders/:id/complete
|
|
|
|
|
|
*/
|
|
|
|
|
|
static async completeOrder(req: Request, res: Response) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { id } = req.params;
|
|
|
|
|
|
const { tenantId } = (req as any).traceContext || { tenantId: 'default-tenant' };
|
2026-03-18 13:38:05 +08:00
|
|
|
|
await OrderService.completeOrder(tenantId, id as string);
|
2026-03-17 22:07:19 +08:00
|
|
|
|
res.json({ success: true, message: 'Order completed successfully' });
|
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
|
res.status(500).json({ success: false, error: err.message });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|