Skip to main content

futu_rest/routes/trd/
cancel_all.rs

1//! REST `/api/cancel-all-order` 便捷写入口。
2//!
3//! 该 route 是 `ModifyOrder(Cancel)` 的 REST convenience wrapper,负责 flat body
4//! promote、强制 cancel-all 字段和写路径限额检查;通用交易 route 不再承载这些局部细节。
5
6use std::sync::Arc;
7
8use axum::Json;
9use axum::extract::{Extension, State};
10use axum::http::HeaderMap;
11use serde_json::Value;
12
13use futu_auth::{CheckCtx, KeyRecord};
14use futu_core::proto_id;
15use futu_proto::trd_modify_order;
16
17use crate::adapter::{self, RestState};
18
19use super::ApiResult;
20use super::card_num::normalize_and_resolve_card_num_for_route;
21use super::validation::{authorize_trade_write, trd_market_str, validate_header_trd_market_write};
22use super::write_pipeline::{
23    idempotency_key_from_headers, limit_reject_response, surface_spec_or_internal_error,
24};
25
26/// v1.4.96 BUG #006 helper (external reviewer 6-round cross-verify, real env data corruption risk fix).
27///
28/// REST `/api/cancel-all-order` 支持两种 body 格式: c2s wrapper 和 flat.
29/// flat body (`{"acc_id":..., "trd_market":1}`) 之前直接传给 adapter, 跳过
30/// handler 的 `c2s.insert("modify_order_op", 2)` 强制覆盖 → daemon 收到
31/// modify_op=0 (ModifyOrder) → real env 真 modify 匹配订单风险.
32///
33/// 本 helper 检测 flat body (顶层有 `acc_id` / `trd_market` / `header` 等
34/// 业务字段, 但无 `c2s` key), 把它包装成 `{"c2s": {"header": {...原字段}}}`
35/// 让强制字段 override 走原 c2s 路径, **避免重复 fix path / silent regression**.
36///
37/// 不动已 c2s wrapper 的 body (`{"c2s": {...}}`).
38///
39/// ## 与 `adapter::maybe_wrap_flat_body_as_c2s` 的关系 (audit follow-up):
40///
41/// `crates/futu-rest/src/adapter.rs::maybe_wrap_flat_body_as_c2s` 已经在
42/// adapter `proto_request` 入口做 flat → c2s wrap, 所以理论上 promote 是
43/// 重复. **但**: cancel-all-order handler 在 `adapter::proto_request` **之前**
44/// 跑 `c2s.insert("modify_order_op", 2)` 强制覆盖, 此时 c2s 必须已存在.
45/// 顺序: handler → `promote_flat_body_to_c2s` → `c2s.insert(...)` →
46/// `adapter::proto_request(body, ...)` → `maybe_wrap_flat_body_as_c2s` (已经
47/// 是 c2s wrapper, no-op).
48///
49/// 因此 `promote_flat_body_to_c2s` 是 cancel-all-order handler 局部需要的,
50/// 不是 adapter 通用 wrap 的替代.
51pub(crate) fn promote_flat_body_to_c2s(body: &mut Value) {
52    // 已有 c2s 则直接返回 (尊重显式 wrapper)
53    if body.get("c2s").is_some() {
54        return;
55    }
56    let Some(map) = body.as_object_mut() else {
57        return;
58    };
59    if map.is_empty() {
60        return;
61    }
62    // 把所有字段下沉到 c2s.header (业务字段 acc_id / trd_market / 等都属
63    // header). 注意: c2s 自己不能在 map 里, 上面已 guard.
64    let header: serde_json::Map<String, Value> = std::mem::take(map).into_iter().collect();
65    let mut c2s = serde_json::Map::new();
66    c2s.insert("header".to_string(), Value::Object(header));
67    map.insert("c2s".to_string(), Value::Object(c2s));
68}
69
70/// POST /api/cancel-all-order — 全部撤单(v1.4.30 新加)
71///
72/// 这是 `/api/modify-order` 的便捷端点,内部强制 `forAll=true` +
73/// `modifyOrderOp=2 (Cancel)` + `orderID=0`,用户只需传 `trdHeader` 和
74/// 可选的 `trdMarket`(不指定市场时撤整个账户全部)。
75///
76/// 风险提示:**对真实账户下发后立即撤销该账户指定市场所有 pending 订单**,
77/// 不可恢复。scope 要求 `trade:real`(同 modify-order),限额按 market
78/// 白名单 + rate 检查。
79pub async fn cancel_all_order(
80    State(state): State<RestState>,
81    rec: Option<Extension<Arc<KeyRecord>>>,
82    headers: HeaderMap,
83    Json(mut body): Json<Value>,
84) -> ApiResult {
85    crate::adapter::normalize_json_keys_snake_case(&mut body);
86    authorize_trade_write(
87        &state,
88        rec.as_ref().map(|Extension(rec)| rec.as_ref()),
89        &body,
90        "/api/cancel-all-order",
91    )?;
92    // v1.4.45: normalize camelCase → snake_case(在强制字段覆盖前做)
93    // v1.4.105 D12 (Phase 2): card_num → acc_id 解析 (与 place_order 一致).
94    // 注意: cancel_all_order 走 flat-body promote 路径, helper 在 normalize 后
95    // 提取 card_num + 写 acc_id 到 top-level (header 不存在则 fallback top),
96    // promote_flat 之后下沉到 c2s.header — 顺序兼容.
97    // v1.4.105 D12 contract-hardening 补丁: 同 place_order, 加 rec 做 string-level
98    // allowed_card_nums whitelist 校验.
99    normalize_and_resolve_card_num_for_route(&state, &rec, &mut body, "/api/cancel-all-order")?;
100    // v1.4.96 BUG #003 hotfix (external reviewer matrix-double-confirmed): trd_market=999
101    // silent accept 200 OK 已修. 在 c2s 强制字段覆盖前 validate, 让用户知道
102    // trd_market 不合法.
103    // v1.4.102 codex 26 F1 (P1): write 路径用更窄 allowlist (无 fund markets)
104    validate_header_trd_market_write(&body, "/api/cancel-all-order")?;
105    // v1.4.96 BUG #006 hotfix (external reviewer 6-round cross-verify 2026-04-26):
106    // flat body (无 c2s wrapper) → 之前 c2s.insert 不命中 → daemon 收到
107    // modify_op=0 (ModifyOrder) 而非 2 (Cancel) → real env 风险真 modify
108    // 匹配的订单. fix: 检测 flat body, 若 acc_id 在顶层而 c2s 不存在, 把
109    // header 字段下沉到 c2s.header 让 adapter 看到统一形态. 然后正常跑
110    // 原有 c2s.insert 强制字段覆盖逻辑.
111    promote_flat_body_to_c2s(&mut body);
112    // 覆盖 c2s 里的强制字段(这里用 snake_case,normalize 后和原代码的 camelCase 等价)
113    if let Some(c2s) = body.get_mut("c2s").and_then(|c| c.as_object_mut()) {
114        c2s.insert("order_id".to_string(), Value::from(0u64));
115        // v1.4.47 P0.2 修(external reviewer 验收报告 §2 跨 5 版未修):modify_order_op 应为 2 (Cancel)
116        // 不是 4。Trd_Common.ModifyOrderOp: 1=Normal, 2=Cancel, 3=Disable, 4=Enable, 5=DeleteOutofdate.
117        // 之前写 4 (Enable) → handler `if modify_op == 2` 不命中 → 走 ReplaceOrder 分支 →
118        // 错误文案硬编码 "ModifyOrder:"(不是 "CancelAllOrder:")。修为 2 后 handler 正确
119        // 命中 cancel_all 分支 → op_name = "CancelAllOrder"。
120        c2s.insert("modify_order_op".to_string(), Value::from(2));
121        c2s.insert("for_all".to_string(), Value::from(true));
122    } else {
123        // v1.4.96 BUG #006: 经 promote_flat_body_to_c2s 后 c2s **必须**存在.
124        // 若仍不存在 = body 完全 empty (如 `{}`), 显式构造让强制字段生效.
125        let mut c2s = serde_json::Map::new();
126        c2s.insert("order_id".to_string(), Value::from(0u64));
127        c2s.insert("modify_order_op".to_string(), Value::from(2));
128        c2s.insert("for_all".to_string(), Value::from(true));
129        if let Some(map) = body.as_object_mut() {
130            map.insert("c2s".to_string(), Value::Object(c2s));
131        }
132    }
133
134    // 限额检查(symbol/value/side 空,仅 market 白名单 + rate)
135    if let Some(Extension(rec)) = rec
136        && let Ok(parsed) = serde_json::from_value::<trd_modify_order::Request>(body.clone())
137    {
138        let market = trd_market_str(parsed.c2s.header.trd_market);
139        let ctx = CheckCtx {
140            market: market.to_string(),
141            symbol: String::new(),
142            order_value: None,
143            trd_side: None,
144            acc_id: Some(parsed.c2s.header.acc_id), // v1.4.35
145            mutation_no_exposure: false,
146            currency: None,
147        };
148        let now = chrono::Utc::now();
149        // v1.4.36 Bug #1:Whitelist → 403
150        let outcome = state
151            .counters
152            .check_full_skip_rate(&rec.id, rec.as_ref(), &ctx, now);
153        if let Some(reason) = outcome.reason() {
154            return Err(limit_reject_response(
155                "/api/cancel-all-order",
156                &rec,
157                &reason,
158                outcome.http_status_code(),
159            ));
160        }
161    }
162
163    let cancel_all_spec =
164        surface_spec_or_internal_error("/api/cancel-all-order", "CancelAllOrder")?;
165    let idem_key = idempotency_key_from_headers(&headers);
166    adapter::proto_request_with_surface_spec_and_idempotency::<
167        trd_modify_order::Request,
168        trd_modify_order::Response,
169    >(
170        &state,
171        proto_id::TRD_MODIFY_ORDER,
172        Some(body),
173        idem_key,
174        cancel_all_spec,
175    )
176    .await
177}
178
179#[cfg(test)]
180mod flat_body_promotion_tests;