diff --git a/crates/fidc-core/src/manual_execution.rs b/crates/fidc-core/src/manual_execution.rs index 733f820..d6f3a31 100644 --- a/crates/fidc-core/src/manual_execution.rs +++ b/crates/fidc-core/src/manual_execution.rs @@ -1,7 +1,7 @@ //! Confirmed manual fills are external observations, not simulated broker fills. //! The producer must bind these records to the runtime's durable order/audit facts. -use std::collections::BTreeSet; +use std::collections::{BTreeMap, BTreeSet}; use chrono::{DateTime, FixedOffset, NaiveDate, Timelike, Utc}; use rust_decimal::Decimal; @@ -12,7 +12,7 @@ use crate::events::OrderSide; use crate::{DataSet, FixedMoney, PortfolioState}; use rust_decimal::prelude::ToPrimitive; -pub const MANUAL_REPLAY_SCHEMA: &str = "fidc.observed-manual-executions/v1"; +pub const MANUAL_REPLAY_SCHEMA: &str = "fidc.observed-manual-executions/v2"; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(deny_unknown_fields, rename_all = "camelCase")] @@ -33,6 +33,7 @@ pub struct ManualExecutionAction { pub source: ManualExecutionSource, pub audit_event_ids: Vec, pub confirmed_at: DateTime, + pub confirmation_observed_at: DateTime, pub outcome: ManualActionOutcome, pub orders: Vec, } @@ -41,6 +42,7 @@ pub struct ManualExecutionAction { #[serde(rename_all = "snake_case")] pub enum ManualActionOutcome { NoOrdersNeeded, + NotExecuted, OrdersTerminal, } @@ -58,12 +60,12 @@ pub enum ManualExecutionSource { pub struct ManualExecutionOrder { pub order_id: String, pub broker_order_id: Option, - pub source_adapter: String, + pub source_adapter: Option, pub symbol: String, pub side: OrderSide, pub quantity: u32, - pub submitted_at: DateTime, - pub terminal_at: DateTime, + pub order_created_at: DateTime, + pub terminal_observed_at: DateTime, pub terminal_status: ManualOrderTerminalStatus, pub fills: Vec, } @@ -83,6 +85,9 @@ pub struct ManualExecutionFill { pub trade_id: String, pub observation_event_id: String, pub observation_sequence: u64, + pub fee_observation_event_id: String, + pub fee_observation_sequence: u64, + pub fee_observed_at: DateTime, pub trade_date: NaiveDate, pub executed_at: DateTime, pub observed_at: DateTime, @@ -90,12 +95,15 @@ pub struct ManualExecutionFill { pub quantity: u32, #[serde(with = "rust_decimal::serde::str")] pub price: Decimal, + #[serde(default, with = "rust_decimal::serde::str_option")] + pub commission: Option, + #[serde(default, with = "rust_decimal::serde::str_option")] + pub stamp_tax: Option, + #[serde(default, with = "rust_decimal::serde::str_option")] + pub transfer_fee: Option, + /// Full observed charge, including any venue fees not itemized above. #[serde(with = "rust_decimal::serde::str")] - pub commission: Decimal, - #[serde(with = "rust_decimal::serde::str")] - pub stamp_tax: Decimal, - #[serde(with = "rust_decimal::serde::str")] - pub transfer_fee: Decimal, + pub total_fee: Decimal, } #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] @@ -126,10 +134,19 @@ impl ManualExecutionFill { } pub fn total_fees(&self) -> Result { - self.commission - .checked_add(self.stamp_tax) - .and_then(|sum| sum.checked_add(self.transfer_fee)) - .ok_or_else(|| "manual fill fees overflow".into()) + let known = [self.commission, self.stamp_tax, self.transfer_fee] + .into_iter() + .flatten() + .try_fold(Decimal::ZERO, |sum, fee| { + if fee < Decimal::ZERO { + return Err("manual fill fee component is negative"); + } + sum.checked_add(fee).ok_or("manual fill fees overflow") + })?; + if self.total_fee < known { + return Err("manual total fee is below its known components".into()); + } + Ok(self.total_fee) } } @@ -200,17 +217,22 @@ impl ManualExecutionReplay { let mut trades = BTreeSet::new(); let mut observation_events = BTreeSet::new(); let mut observation_sequences = BTreeSet::new(); + let mut fee_observations = BTreeSet::new(); + let mut receipt_ids = BTreeMap::new(); + let mut receipt_sequences = BTreeMap::new(); for action in &self.actions { identifier(&action.action_id)?; if !actions.insert(action.action_id.as_str()) || action.confirmed_at > self.observation_cutoff + || action.confirmation_observed_at < action.confirmed_at + || action.confirmation_observed_at > self.observation_cutoff { return Err("duplicate manual action or confirmation after cutoff".into()); } if action.audit_event_ids.is_empty() { return Err("manual action has no immutable audit binding".into()); } - if (action.outcome == ManualActionOutcome::NoOrdersNeeded) != action.orders.is_empty() { + if (action.outcome != ManualActionOutcome::OrdersTerminal) != action.orders.is_empty() { return Err("manual action outcome does not prove its order coverage".into()); } for id in &action.audit_event_ids { @@ -221,20 +243,28 @@ impl ManualExecutionReplay { } for order in &action.orders { identifier(&order.order_id)?; - identifier(&order.source_adapter)?; + if let Some(adapter) = &order.source_adapter { + identifier(adapter)?; + } identifier(&order.symbol)?; if let Some(id) = &order.broker_order_id { identifier(id)?; if !broker_orders.insert(( - order.source_adapter.as_str(), - order.submitted_at.with_timezone(&shanghai).date_naive(), + order + .source_adapter + .as_deref() + .ok_or("broker identity requires its source adapter")?, + order.order_created_at.with_timezone(&shanghai).date_naive(), id.as_str(), )) { return Err("manual local orders share one broker order identity".into()); } } + if !order.fills.is_empty() && order.source_adapter.is_none() { + return Err("manual fills require a known source adapter".into()); + } if !order.fills.is_empty() - && order.source_adapter != "paper" + && order.source_adapter.as_deref() != Some("paper") && order.broker_order_id.is_none() { return Err( @@ -247,9 +277,9 @@ impl ManualExecutionReplay { { return Err("duplicate manual order or invalid quantity".into()); } - if order.submitted_at < action.confirmed_at - || order.terminal_at < order.submitted_at - || order.terminal_at > self.observation_cutoff + if order.order_created_at < action.confirmed_at + || order.terminal_observed_at < order.order_created_at + || order.terminal_observed_at > self.observation_cutoff { return Err( "manual order confirmation/submission/terminal time is inconsistent".into(), @@ -259,6 +289,7 @@ impl ManualExecutionReplay { for fill in &order.fills { identifier(&fill.trade_id)?; identifier(&fill.observation_event_id)?; + identifier(&fill.fee_observation_event_id)?; if fill.observation_sequence == 0 || fill.observation_sequence > i64::MAX as u64 || !observation_events.insert(fill.observation_event_id.as_str()) @@ -269,16 +300,52 @@ impl ManualExecutionReplay { .into(), ); } + if fill.fee_observation_sequence == 0 + || fill.fee_observation_sequence > i64::MAX as u64 + || fill.fee_observed_at < fill.observed_at + || fill.fee_observed_at > self.observation_cutoff + || !fee_observations.insert(( + fill.fee_observation_event_id.as_str(), + fill.fee_observation_sequence, + )) + { + return Err("manual finalized fees require their own unique observation within the cutoff".into()); + } + if (fill.fee_observation_event_id == fill.observation_event_id) + != (fill.fee_observation_sequence == fill.observation_sequence) + || (fill.fee_observation_event_id == fill.observation_event_id + && fill.fee_observed_at != fill.observed_at) + { + return Err("manual fill and fee observation identities disagree".into()); + } if !trades.insert((fill.trade_date, fill.trade_id.as_str())) || fill.quantity == 0 { return Err("duplicate manual trade or zero fill quantity".into()); } + for (id, sequence) in [ + (&fill.observation_event_id, fill.observation_sequence), + ( + &fill.fee_observation_event_id, + fill.fee_observation_sequence, + ), + ] { + if receipt_ids + .insert(id, (&fill.trade_id, sequence)) + .is_some_and(|owner| owner != (&fill.trade_id, sequence)) + || receipt_sequences + .insert(sequence, (&fill.trade_id, id)) + .is_some_and(|owner| owner != (&fill.trade_id, id)) + { + return Err("manual observation identity is reused by a different trade or sequence".into()); + } + } if fill.executed_at.with_timezone(&shanghai).date_naive() != fill.trade_date || fill.observed_at > self.observation_cutoff - || fill.observed_at < order.submitted_at + || fill.observed_at < order.order_created_at + || fill.observed_at < action.confirmation_observed_at || fill.observed_at < fill.executed_at - || fill.executed_at > order.terminal_at + || fill.executed_at > order.terminal_observed_at { return Err("manual fill execution/observation time is inconsistent".into()); } @@ -297,18 +364,12 @@ impl ManualExecutionReplay { fill.timestamp_precision.nanoseconds(), )) .ok_or("manual execution timestamp overflow")?; - if fill.executed_at < order.submitted_at && order.submitted_at >= upper { - return Err("manual fill predates its submitted order".into()); + let earliest = order.order_created_at.max(action.confirmation_observed_at); + if fill.executed_at < earliest && earliest >= upper { + return Err("manual fill predates its order or durable confirmation".into()); } - if fill.price <= Decimal::ZERO - || [fill.commission, fill.stamp_tax, fill.transfer_fee] - .iter() - .any(|fee| *fee < Decimal::ZERO) - { - return Err( - "manual fill requires a positive price and complete nonnegative fees" - .into(), - ); + if fill.price <= Decimal::ZERO { + return Err("manual fill requires a positive price".into()); } fill.gross_amount()? .checked_add(fill.total_fees()?) @@ -367,15 +428,18 @@ pub struct ManualReplayApplication { pub observation_event_id: String, pub observation_sequence: u64, pub observed_at: DateTime, + pub fee_observation_event_id: String, + pub fee_observed_at: DateTime, pub executed_at: DateTime, pub symbol: String, pub side: OrderSide, pub quantity: u32, pub quantity_after: u32, pub price: String, - pub commission: String, - pub stamp_tax: String, - pub transfer_fee: String, + pub commission: Option, + pub stamp_tax: Option, + pub transfer_fee: Option, + pub source_total_fee: String, pub source_gross_amount: String, pub ledger_gross_amount: String, pub ledger_fees: String, @@ -458,15 +522,18 @@ impl ManualReplayCursor { observation_event_id: fill.observation_event_id.clone(), observation_sequence: fill.observation_sequence, observed_at: fill.observed_at, + fee_observation_event_id: fill.fee_observation_event_id.clone(), + fee_observed_at: fill.fee_observed_at, executed_at: fill.executed_at, symbol: order.symbol.clone(), side: order.side, quantity: fill.quantity, quantity_after: applied.quantity_after, price: fill.price.to_string(), - commission: fill.commission.to_string(), - stamp_tax: fill.stamp_tax.to_string(), - transfer_fee: fill.transfer_fee.to_string(), + commission: fill.commission.map(|fee| fee.to_string()), + stamp_tax: fill.stamp_tax.map(|fee| fee.to_string()), + transfer_fee: fill.transfer_fee.map(|fee| fee.to_string()), + source_total_fee: fill.total_fee.to_string(), source_gross_amount: fill.gross_amount()?.to_string(), ledger_gross_amount: applied.gross.to_decimal_string(), ledger_fees: applied.fees.to_decimal_string(), diff --git a/crates/fidc-core/src/manual_execution/tests.rs b/crates/fidc-core/src/manual_execution/tests.rs index 0bdb5a2..44e5055 100644 --- a/crates/fidc-core/src/manual_execution/tests.rs +++ b/crates/fidc-core/src/manual_execution/tests.rs @@ -2,16 +2,18 @@ use super::*; use serde_json::{Value, json}; fn sample() -> ManualExecutionReplay { + let fill = json!({"tradeId":"trade-1","observationEventId":"received-1","observationSequence":1,"tradeDate":"2026-09-14","executedAt":"2026-09-14T01:30:00Z", + "feeObservationEventId":"received-1","feeObservationSequence":1,"feeObservedAt":"2026-09-14T01:30:01Z", + "observedAt":"2026-09-14T01:30:01Z","timestampPrecision":"second","quantity":100, + "price":"10.1234567891","commission":"0.1000001","stampTax":"0","transferFee":"0.02","totalFee":"0.1200001"}); let mut input:ManualExecutionReplay=serde_json::from_value(json!({ "schema":MANUAL_REPLAY_SCHEMA,"runtimeId":"runtime-1","accountId":"account-1", "sourceContractSha256":"a".repeat(64),"contentSha256":"", "observationCutoff":"2026-09-14T08:00:00Z", "actions":[{"actionId":"action-1","source":"manual_security_trade","auditEventIds":["audit-1"], - "confirmedAt":"2026-09-14T01:30:00.500Z","outcome":"orders_terminal","orders":[{ + "confirmedAt":"2026-09-14T01:30:00.500Z","confirmationObservedAt":"2026-09-14T01:30:00.550Z","outcome":"orders_terminal","orders":[{ "orderId":"order-1","brokerOrderId":"broker-1","sourceAdapter":"gt-api","symbol":"000001.SZ","side":"Buy","quantity":100, - "submittedAt":"2026-09-14T01:30:00.600Z","terminalAt":"2026-09-14T01:30:00.900Z","terminalStatus":"filled", - "fills":[{"tradeId":"trade-1","observationEventId":"received-1","observationSequence":1,"tradeDate":"2026-09-14","executedAt":"2026-09-14T01:30:00Z", - "observedAt":"2026-09-14T01:30:01Z","timestampPrecision":"second","quantity":100, - "price":"10.1234567891","commission":"0.1000001","stampTax":"0","transferFee":"0.02"}] + "orderCreatedAt":"2026-09-14T01:30:00.600Z","terminalObservedAt":"2026-09-14T01:30:00.900Z","terminalStatus":"filled", + "fills":[fill] }]}] })).unwrap(); reseal(&mut input); @@ -43,7 +45,7 @@ fn complete_exact_decimal_evidence_allows_later_observation_and_retains_source_d #[test] fn all_required_money_and_binding_fields_reject_missing_or_wrong_values() { let original = serde_json::to_value(sample()).unwrap(); - for field in ["price", "commission", "stampTax", "transferFee"] { + for field in ["price", "totalFee"] { let mut missing = original.clone(); missing["actions"][0]["orders"][0]["fills"][0] .as_object_mut() @@ -96,7 +98,7 @@ fn inconsistent_counts_terminals_audits_and_duplicate_facts_are_rejected() { let mut invalid = original.clone(); invalid.actions[0].orders[0].broker_order_id = None; assert!(semantic_result(&invalid).is_err()); - invalid.actions[0].orders[0].source_adapter = "paper".into(); + invalid.actions[0].orders[0].source_adapter = Some("paper".into()); reseal(&mut invalid); invalid.validate().unwrap(); } @@ -104,12 +106,14 @@ fn inconsistent_counts_terminals_audits_and_duplicate_facts_are_rejected() { #[test] fn source_time_precision_is_not_invented_and_submitted_time_must_fit_the_interval() { let mut input = sample(); - input.actions[0].orders[0].submitted_at = "2026-09-14T01:30:00.999999Z".parse().unwrap(); - input.actions[0].orders[0].terminal_at = "2026-09-14T01:30:01.500Z".parse().unwrap(); + input.actions[0].orders[0].order_created_at = "2026-09-14T01:30:00.999999Z".parse().unwrap(); + input.actions[0].orders[0].terminal_observed_at = "2026-09-14T01:30:01.500Z".parse().unwrap(); input.actions[0].orders[0].fills[0].observed_at = "2026-09-14T01:30:02Z".parse().unwrap(); + input.actions[0].orders[0].fills[0].fee_observed_at = + input.actions[0].orders[0].fills[0].observed_at; reseal(&mut input); input.validate().unwrap(); - input.actions[0].orders[0].submitted_at = "2026-09-14T01:30:01Z".parse().unwrap(); + input.actions[0].orders[0].order_created_at = "2026-09-14T01:30:01Z".parse().unwrap(); assert!(semantic_result(&input).is_err()); let mut input = sample(); input.actions[0].orders[0].fills[0].executed_at = "2026-09-14T01:30:00.800Z".parse().unwrap(); @@ -130,6 +134,9 @@ fn confirmed_no_order_outcome_is_distinct_from_unconfirmed_or_unknown_work() { input.actions[0].outcome = ManualActionOutcome::NoOrdersNeeded; reseal(&mut input); input.validate().unwrap(); + input.actions[0].outcome = ManualActionOutcome::NotExecuted; + reseal(&mut input); + input.validate().unwrap(); let mut value = serde_json::to_value(input).unwrap(); value["actions"][0]["outcome"] = json!("result_unknown"); assert!(serde_json::from_value::(value).is_err()); @@ -144,10 +151,83 @@ fn raw_timezone_and_cutoff_are_required() { input.observation_cutoff = "2026-09-14T01:30:00.700Z".parse().unwrap(); assert!(semantic_result(&input).is_err()); let mut value = serde_json::to_value(sample()).unwrap(); - value["actions"][0]["orders"][0]["fills"][0]["commission"] = Value::Null; + value["actions"][0]["orders"][0]["fills"][0]["totalFee"] = Value::Null; assert!(serde_json::from_value::(value).is_err()); } +#[test] +fn authoritative_total_fee_does_not_require_inventing_unknown_components() { + let mut input = sample(); + let fill = &mut input.actions[0].orders[0].fills[0]; + fill.commission = None; + fill.stamp_tax = None; + fill.transfer_fee = None; + assert_eq!( + fill.total_fees().unwrap(), + "0.1200001".parse::().unwrap() + ); + assert!(semantic_result(&input).is_ok()); + let value = serde_json::to_value(&input).unwrap(); + assert!(value["actions"][0]["orders"][0]["fills"][0]["commission"].is_null()); + assert_eq!( + value["actions"][0]["orders"][0]["fills"][0]["totalFee"], + "0.1200001" + ); + for field in ["commission", "stampTax", "transferFee"] { + let mut numeric = value.clone(); + numeric["actions"][0]["orders"][0]["fills"][0][field] = json!(0.1); + assert!(serde_json::from_value::(numeric).is_err()); + } +} + +#[test] +fn manual_fee_total_includes_extra_charges_and_rejects_inconsistent_components() { + let mut input = sample(); + input.actions[0].orders[0].fills[0].total_fee = "0.15".parse().unwrap(); + assert!(semantic_result(&input).is_ok()); + assert_eq!( + input.actions[0].orders[0].fills[0] + .total_fees() + .unwrap() + .to_string(), + "0.15" + ); + input.actions[0].orders[0].fills[0].total_fee = "0.1".parse().unwrap(); + assert!(semantic_result(&input).is_err()); + input.actions[0].orders[0].fills[0].total_fee = "0.15".parse().unwrap(); + input.actions[0].orders[0].fills[0].commission = Some(Decimal::NEGATIVE_ONE); + assert!(semantic_result(&input).is_err()); +} + +#[test] +fn late_fee_evidence_keeps_the_original_fill_observation_clock() { + let data = identity_data(NaiveDate::from_ymd_opt(2020, 1, 1).unwrap()); + let mut input = sample(); + let fill = &mut input.actions[0].orders[0].fills[0]; + let original = fill.observed_at; + fill.fee_observation_event_id = "fee-receipt-1".into(); + fill.fee_observation_sequence = 2; + fill.fee_observed_at = original + chrono::Duration::hours(1); + let fee_time = fill.fee_observed_at; + reseal(&mut input); + let mut cursor = ManualReplayCursor::new(input).unwrap(); + assert_eq!(cursor.next_observation_at(), Some(original)); + let mut portfolio = PortfolioState::new(10_000.); + let result = cursor + .advance(original, &mut portfolio, &data, false) + .unwrap(); + assert_eq!(result.len(), 1); + assert_eq!(result[0].observed_at, original); + assert_eq!(result[0].fee_observed_at, fee_time); + assert_eq!(result[0].source_total_fee, "0.1200001"); + assert!( + cursor + .advance(fee_time, &mut portfolio, &data, false) + .unwrap() + .is_empty() + ); +} + #[test] fn changing_any_external_price_or_identity_invalidates_the_frozen_trace() { let input = sample(); @@ -263,11 +343,12 @@ fn the_next_day_manual_sale_keeps_the_actual_quantity_and_fee_contract() { let mut sell = input.clone(); let order = &mut sell.actions[0].orders[0]; order.side = OrderSide::Sell; - order.submitted_at += chrono::Duration::days(1); - order.terminal_at += chrono::Duration::days(1); + order.order_created_at += chrono::Duration::days(1); + order.terminal_observed_at += chrono::Duration::days(1); order.fills[0].trade_date = order.fills[0].trade_date.succ_opt().unwrap(); order.fills[0].executed_at += chrono::Duration::days(1); order.fills[0].observed_at += chrono::Duration::days(1); + order.fills[0].fee_observed_at += chrono::Duration::days(1); sell.observation_cutoff += chrono::Duration::days(1); reseal(&mut sell); let applied = sell.observations().unwrap()[0] @@ -285,6 +366,8 @@ fn observations_follow_durable_receipt_order_and_not_input_array_order() { second.trade_id = "trade-2".into(); second.observation_event_id = "received-2".into(); second.observation_sequence = 2; + second.fee_observation_event_id = "received-2".into(); + second.fee_observation_sequence = 2; input.actions[0].orders[0].quantity = 200; input.actions[0].orders[0].fills.insert(0, second); reseal(&mut input); @@ -380,6 +463,8 @@ fn failed_multi_receipt_advance_keeps_both_progress_and_portfolio_unchanged() { next.trade_id = "trade-2".into(); next.observation_event_id = "received-2".into(); next.observation_sequence = 2; + next.fee_observation_event_id = "received-2".into(); + next.fee_observation_sequence = 2; input.actions[0].orders[0].quantity = 200; input.actions[0].orders[0].fills.push(next); reseal(&mut input); diff --git a/docs/manual-replay-foundation-20260914.md b/docs/manual-replay-foundation-20260914.md index b2721c4..2eaef9e 100644 --- a/docs/manual-replay-foundation-20260914.md +++ b/docs/manual-replay-foundation-20260914.md @@ -1,10 +1,18 @@ # 手工成交观察回放:基础合同与当前断点 -2026-09-14。本阶段只完成框架基础与本机验证,未接入Runner/API、未发布。生产最近已验收版本仍为v2026.9.14.5;完整Goal和手工影子回放均未完成。 +2026-09-14。当前候选已升级v2并与交易端权威读取配套,仍未接入Runner/API或引擎主时钟、未发布。交易最近发布是166998d/v2026.9.14.6,回测仍81acc54/e81;完整Goal和手工影子回放均未完成。 + +## v2读取合同补充 + +总费用必须来自权威事实,佣金/印花税/过户费等组件可以未知,不能反过来用已知组件推定费用完整。保留组件原精度、总费用和微元账本费用;未知组件不写成0。新增费用来源事件/序号/可见时刻,原FillReceived继续决定股数变化时刻,后补费用不推迟成交、也不重复入账。历史采用最终费用回放口径,不能声称费用明细当时已经可见。 + +分别表达订单创建、确认登记、成交、原始观察、费用观察与终态核对,不伪装GT实际发送时间。无订单区分NoOrdersNeeded与NotExecuted;无成交且无券商身份时允许适配器未知,不造名称。确认登记之前的成交、证据跨交易复用、费用少于已知组件及越截止点均拒绝。 + +最新main a29c434的DayOpen和列存变更已按ff-only保留合入;组合Core860通过,其中本模块18项。交易端读取四类来源及验证范围见fidc-trading-platform/docs/manual-replay-capture-20260914.md。未将整仓无订单、Paper一例与Live一例外推完整参数/时钟/券商验收,不据此解除门禁。 ## 已实现 -`manual_execution`提供`fidc.observed-manual-executions/v1`严格合同及`ManualReplayCursor`。这是将已确认的手工成交事实作为外部输入,不是让回测券商独立重演其真实成交。 +`manual_execution`提供`fidc.observed-manual-executions/v2`严格合同及`ManualReplayCursor`。这是将已确认的手工成交事实作为外部输入,不是让回测券商独立重演其真实成交。下面保留初版阶段的实现说明,费用和时间字段以本节v2补充为准。 - 保留确认、提交、成交、观察和终态时间,声明秒/毫秒/微秒/纳秒精度;同秒报告只允许在其真实精度区间内与提交时间对应,不伪造纳秒。 - 手工动作、审计事件、订单、券商订单、成交和`FillReceived`观察事件/序号均有唯一性与完整性校验。账户/运行身份及源合同摘要进入完整内容SHA;改价格、费用、身份或时间会使旧摘要失效。