feat(payment): P2 — 订单管理增强,支持取消订单和订单详情查询

## 后端改动

### PaymentClientService 增强
- 新增 `getOrderDetail(orderId)` — 获取完整订单信息(含支付详情)
- 新增 `cancelOrder(orderId)` — 取消未支付订单(调用 POST /orders/:id/cancel)

### 新增 cancel_order 工具
- 工具定义: 接收 orderId,取消未支付订单
- 实现: 调用 PaymentClientService.cancelOrder()
- 成功返回 { success, orderId, status, message }
- 失败返回友好错误信息(如"只有未支付的订单才能取消")
- coordinator-tools.ts 注册,concurrency map 标记 false(写操作)

## 前端改动

### cancel_order 结果渲染
- 成功: 绿色卡片 + CheckCircle 图标 + 成功提示
- 失败: 红色卡片 + AlertCircle 图标 + 错误原因
- 显示订单号

## 注意事项
- payment-service 暂无退款 API,cancel_order 仅限未支付订单
- 退款功能待 payment-service 侧实现后再扩展

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-02-07 01:23:28 -08:00
parent db7964a461
commit a3f2be078b
5 changed files with 144 additions and 0 deletions

View File

@ -386,6 +386,19 @@ export const DIRECT_TOOLS: ToolDefinition[] = [
},
isConcurrencySafe: true, // 只读
},
{
name: 'cancel_order',
description:
'取消未支付的订单。仅限未支付状态可取消。用户明确要求取消时使用。',
input_schema: {
type: 'object',
properties: {
orderId: { type: 'string', description: '要取消的订单 ID' },
},
required: ['orderId'],
},
isConcurrencySafe: false, // 写操作
},
];
// ============================================================

View File

@ -276,4 +276,5 @@ export const TOOL_CONCURRENCY_MAP: Record<string, boolean> = {
save_user_memory: false, // 写操作
generate_payment: false, // 创建支付订单
collect_assessment_info: false, // 写操作
cancel_order: false, // 取消订单
};

View File

@ -154,6 +154,20 @@ export class ImmigrationToolsService {
properties: {},
},
},
{
name: 'cancel_order',
description: '取消未支付的订单。仅限状态为待支付的订单可以取消。当用户明确表示要取消订单时使用。',
input_schema: {
type: 'object',
properties: {
orderId: {
type: 'string',
description: '要取消的订单ID',
},
},
required: ['orderId'],
},
},
{
name: 'save_user_memory',
description: '保存用户的重要信息到长期记忆,以便后续对话中记住用户情况',
@ -306,6 +320,9 @@ export class ImmigrationToolsService {
case 'query_order_history':
return this.queryOrderHistory(context);
case 'cancel_order':
return this.cancelOrder(input);
case 'save_user_memory':
return this.saveUserMemory(input, context);
@ -615,6 +632,36 @@ export class ImmigrationToolsService {
};
}
/**
* Cancel order
*/
private async cancelOrder(
input: Record<string, unknown>,
): Promise<unknown> {
const { orderId } = input as { orderId: string };
console.log(`[Tool:cancel_order] Order: ${orderId}`);
if (!this.paymentClient) {
return { success: false, error: '支付服务暂不可用' };
}
const result = await this.paymentClient.cancelOrder(orderId);
if (!result) {
return {
success: false,
error: '取消订单失败。只有未支付的订单才能取消,请确认订单状态。',
};
}
return {
success: true,
orderId: result.orderId,
status: result.status,
message: '订单已成功取消',
};
}
/**
* Save user memory - knowledge-service Memory API
*/

View File

@ -223,4 +223,57 @@ export class PaymentClientService implements OnModuleInit {
return [];
}
}
/**
*
*/
async getOrderDetail(orderId: string): Promise<OrderInfo | null> {
try {
const response = await fetch(`${this.baseUrl}/orders/${orderId}`);
if (!response.ok) {
console.error(
`[PaymentClient] getOrderDetail failed: ${response.status}`,
);
return null;
}
const data = (await response.json()) as ApiResponse<OrderInfo>;
return data.success ? data.data : null;
} catch (error) {
console.error('[PaymentClient] getOrderDetail error:', error);
return null;
}
}
/**
*
*/
async cancelOrder(
orderId: string,
): Promise<{ orderId: string; status: string } | null> {
try {
const response = await fetch(
`${this.baseUrl}/orders/${orderId}/cancel`,
{ method: 'POST' },
);
if (!response.ok) {
const errText = await response.text();
console.error(
`[PaymentClient] cancelOrder failed: ${response.status} ${errText}`,
);
return null;
}
const data = (await response.json()) as ApiResponse<{
orderId: string;
status: string;
}>;
return data.success ? data.data : null;
} catch (error) {
console.error('[PaymentClient] cancelOrder error:', error);
return null;
}
}
}

View File

@ -397,5 +397,35 @@ function ToolCallResult({
}
}
if (toolCall.name === 'cancel_order') {
const result = toolCall.result as {
success?: boolean;
orderId?: string;
message?: string;
error?: string;
};
return (
<div className={clsx(
'mt-3 p-3 rounded-lg border',
result.success ? 'bg-green-50 border-green-200' : 'bg-red-50 border-red-200',
)}>
<div className="flex items-center gap-2">
{result.success ? (
<CheckCircle className="w-4 h-4 text-green-500" />
) : (
<AlertCircle className="w-4 h-4 text-red-500" />
)}
<span className="text-sm">
{result.success ? result.message : result.error}
</span>
</div>
{result.orderId && (
<p className="mt-1 text-xs text-secondary-500">: {result.orderId}</p>
)}
</div>
);
}
return null;
}