From 64a4ca5c0a523ffc51c861eac1291671b90a2f3a Mon Sep 17 00:00:00 2001 From: "Somhairle H. Marisol" Date: Mon, 21 Sep 2026 06:38:58 +0800 Subject: fix(api,web): 下单/读取请求身份隔离与零手续费支持(3d-1 评审修正) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Web: orderSeq 拆分为 orderCreateSeq/orderReadSeq,并发刷新不再丢弃在途下单结果;换基金/换 token 双序列失效并携带 fundId 双重守卫 - 零手续费:DB CHECK fee_amount >= 0(幂等迁移),校验改为拒绝负数,UI 提示与占位符同步 - 测试:API 增加零手续费回放与 DB 负手续费约束回归(43/43),浏览器新增 H 系列延迟 POST 并发刷新/失败恢复/换基金隔离(78/78) --- src/FundLab.Web/App.fs | 57 +++++++++++++++++++++++++++++++------------------- 1 file changed, 36 insertions(+), 21 deletions(-) (limited to 'src/FundLab.Web/App.fs') diff --git a/src/FundLab.Web/App.fs b/src/FundLab.Web/App.fs index 5a89b5b..51113a0 100644 --- a/src/FundLab.Web/App.fs +++ b/src/FundLab.Web/App.fs @@ -228,7 +228,8 @@ type Model = orderCode: string orderAmount: string orderFee: string - orderSeq: int + orderCreateSeq: int + orderReadSeq: int orderInFlight: bool lastOrderAttempt: OrderAttempt option pendingOrders: RawOrder list @@ -258,8 +259,8 @@ type Msg = | OrderAmountChanged of string | OrderFeeChanged of string | OrderCreateRequested - | OrderCreateCompleted of requestId: int * order: RawOrder - | OrderCreateFailed of requestId: int * message: string + | OrderCreateCompleted of requestId: int * fundId: string * order: RawOrder + | OrderCreateFailed of requestId: int * fundId: string * message: string | OrdersReadRequested | OrdersReadCompleted of requestId: int * orders: RawOrder array | OrdersReadFailed of requestId: int * message: string @@ -276,6 +277,11 @@ let isPositiveCash (text: string) = | true, value -> value > 0M | false, _ -> false +let isNonNegativeCash (text: string) = + match Decimal.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture) with + | true, value -> value >= 0M + | false, _ -> false + let resolveCreateKey (lastAttempt: CreateAttempt option) (name: string) (cash: string) = match lastAttempt with | Some attempt when attempt.name = name && attempt.cash = cash -> attempt.idempotencyKey @@ -307,7 +313,8 @@ let init () = orderCode = "" orderAmount = "" orderFee = "" - orderSeq = 0 + orderCreateSeq = 0 + orderReadSeq = 0 orderInFlight = false lastOrderAttempt = None pendingOrders = [] @@ -359,8 +366,8 @@ let private createOrderCommand token fundId payload requestId = Cmd.OfPromise.either (fun () -> Api.createOrder token fundId payload) () - (fun order -> OrderCreateCompleted(requestId, order)) - (fun error -> OrderCreateFailed(requestId, errorText error)) + (fun order -> OrderCreateCompleted(requestId, fundId, order)) + (fun error -> OrderCreateFailed(requestId, fundId, errorText error)) let private readOrdersCommand token fundId requestId = Cmd.OfPromise.either @@ -391,7 +398,8 @@ let update message model = orderCode = "" orderAmount = "" orderFee = "" - orderSeq = model.orderSeq + 1 + orderCreateSeq = model.orderCreateSeq + 1 + orderReadSeq = model.orderReadSeq + 1 orderInFlight = false lastOrderAttempt = None pendingOrders = [] @@ -543,6 +551,11 @@ let update message model = createdFund = Some fund fundInFlight = false lastCreateAttempt = None + orderCreateSeq = model.orderCreateSeq + 1 + orderReadSeq = model.orderReadSeq + 1 + orderInFlight = false + lastOrderAttempt = None + pendingOrders = [] error = None }, Cmd.ofMsg OrdersReadRequested @@ -596,12 +609,12 @@ let update message model = { model with error = Some "请输入基金代码" }, Cmd.none elif not (isValidCashText amount) || not (isPositiveCash amount) then { model with error = Some "认购金额必须是大于零的两位小数金额,例如 100.00" }, Cmd.none - elif not (isValidCashText fee) || not (isPositiveCash fee) then - { model with error = Some "认购手续费必须是大于零的两位小数金额,例如 1.50" }, Cmd.none + elif not (isValidCashText fee) || not (isNonNegativeCash fee) then + { model with error = Some "认购手续费必须是不小于零的两位小数金额,例如 0.00" }, Cmd.none elif model.orderInFlight then model, Cmd.none else - let requestId = model.orderSeq + 1 + let requestId = model.orderCreateSeq + 1 let idempotencyKey = resolveOrderKey model.lastOrderAttempt code amount fee { @@ -609,7 +622,7 @@ let update message model = orderCode = code orderAmount = amount orderFee = fee - orderSeq = requestId + orderCreateSeq = requestId orderInFlight = true lastOrderAttempt = Some @@ -631,8 +644,9 @@ let update message model = feeAmount = fee } requestId - | OrderCreateCompleted (requestId, order) -> - if requestId = model.orderSeq then + | OrderCreateCompleted (requestId, fundId, order) -> + if requestId = model.orderCreateSeq + && (match model.createdFund with Some fund -> fund.id = fundId | None -> false) then { model with orderInFlight = false @@ -642,24 +656,25 @@ let update message model = Cmd.batch [ Cmd.ofMsg FundReadRequested; Cmd.ofMsg OrdersReadRequested ] else model, Cmd.none - | OrderCreateFailed (requestId, message) -> - if requestId = model.orderSeq then + | OrderCreateFailed (requestId, fundId, message) -> + if requestId = model.orderCreateSeq + && (match model.createdFund with Some fund -> fund.id = fundId | None -> false) then { model with orderInFlight = false; error = Some message }, Cmd.none else model, Cmd.none | OrdersReadRequested -> match model.createdFund with | Some fund when not (String.IsNullOrWhiteSpace model.token) -> - let requestId = model.orderSeq + 1 + let requestId = model.orderReadSeq + 1 - { model with orderSeq = requestId; error = None }, + { model with orderReadSeq = requestId; error = None }, readOrdersCommand model.token fund.id requestId | Some _ -> { model with error = Some "请输入 API token" }, Cmd.none | None -> model, Cmd.none | OrdersReadCompleted (requestId, orders) -> - if requestId = model.orderSeq then + if requestId = model.orderReadSeq then { model with pendingOrders = orders |> Array.toList @@ -669,7 +684,7 @@ let update message model = else model, Cmd.none | OrdersReadFailed (requestId, message) -> - if requestId = model.orderSeq then + if requestId = model.orderReadSeq then { model with error = Some message }, Cmd.none else model, Cmd.none @@ -1068,7 +1083,7 @@ let private subscribePanel model dispatch = Html.span "认购手续费(元)" Html.input [ prop.className "text-input order-fee-input" - prop.placeholder "例如 1.50" + prop.placeholder "0.00 或 1.50" prop.value model.orderFee prop.onChange (fun value -> dispatch (OrderFeeChanged value)) ] @@ -1084,7 +1099,7 @@ let private subscribePanel model dispatch = ] Html.p [ prop.className "hint" - prop.text "金额与手续费按两位小数字符串提交;下单即冻结现金,订单保持已提交 · 待确认,不产生任何份额或收益。" + prop.text "金额与手续费按两位小数字符串提交,手续费允许 0.00;下单即冻结现金,订单保持已提交 · 待确认,不产生任何份额或收益。" ] Html.div [ prop.className "pending-orders" -- cgit v1.2.3