1
This commit is contained in:
@@ -1396,9 +1396,13 @@ pub async fn use_puzzle_runtime_prop(
|
||||
));
|
||||
}
|
||||
};
|
||||
let should_sync_freeze_boundary = matches!(prop_kind.as_str(), "freezeTime" | "freeze_time");
|
||||
let billing_asset_id = format!("{}:{}:{}", run_id, prop_kind, current_utc_micros());
|
||||
let reducer_owner_user_id = owner_user_id.clone();
|
||||
let run = execute_billable_asset_operation(
|
||||
let reducer_run_id = run_id.clone();
|
||||
let fallback_run_id = run_id.clone();
|
||||
let fallback_owner_user_id = owner_user_id.clone();
|
||||
let run_result = execute_billable_asset_operation(
|
||||
&state,
|
||||
&owner_user_id,
|
||||
billing_asset_kind,
|
||||
@@ -1407,7 +1411,7 @@ pub async fn use_puzzle_runtime_prop(
|
||||
state
|
||||
.spacetime_client()
|
||||
.use_puzzle_runtime_prop(PuzzleRunPropRecordInput {
|
||||
run_id,
|
||||
run_id: reducer_run_id,
|
||||
owner_user_id: reducer_owner_user_id,
|
||||
prop_kind,
|
||||
used_at_micros: current_utc_micros(),
|
||||
@@ -1417,8 +1421,30 @@ pub async fn use_puzzle_runtime_prop(
|
||||
.map_err(map_puzzle_client_error)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(|error| puzzle_error_response(&request_context, PUZZLE_RUNTIME_PROVIDER, error))?;
|
||||
.await;
|
||||
|
||||
let run = match run_result {
|
||||
Ok(run) => run,
|
||||
Err(error) if should_sync_puzzle_freeze_boundary(&error, should_sync_freeze_boundary) => {
|
||||
// 中文注释:冻结确认窗打开时前端会暂停视觉计时,但正式 run 仍可能在服务端边界帧先结算失败。
|
||||
// 这类情况已由扣费包装器退款,此处只同步失败态快照,避免玩家看到“操作不合法”。
|
||||
state
|
||||
.spacetime_client()
|
||||
.get_puzzle_run(fallback_run_id, fallback_owner_user_id)
|
||||
.await
|
||||
.map_err(map_puzzle_client_error)
|
||||
.map_err(|error| {
|
||||
puzzle_error_response(&request_context, PUZZLE_RUNTIME_PROVIDER, error)
|
||||
})?
|
||||
}
|
||||
Err(error) => {
|
||||
return Err(puzzle_error_response(
|
||||
&request_context,
|
||||
PUZZLE_RUNTIME_PROVIDER,
|
||||
error,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(json_success_body(
|
||||
Some(&request_context),
|
||||
@@ -2503,6 +2529,10 @@ fn map_puzzle_client_error(error: SpacetimeClientError) -> AppError {
|
||||
}))
|
||||
}
|
||||
|
||||
fn should_sync_puzzle_freeze_boundary(error: &AppError, is_freeze_time: bool) -> bool {
|
||||
is_freeze_time && error.body_text().contains("操作不合法")
|
||||
}
|
||||
|
||||
fn is_missing_puzzle_form_draft_procedure_error(error: &SpacetimeClientError) -> bool {
|
||||
matches!(error, SpacetimeClientError::Procedure(message) if
|
||||
message.contains("save_puzzle_form_draft")
|
||||
@@ -3580,6 +3610,26 @@ mod tests {
|
||||
let response = error.into_response();
|
||||
assert_eq!(response.status(), StatusCode::BAD_GATEWAY);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn freeze_boundary_sync_only_matches_freeze_invalid_operation() {
|
||||
let invalid_operation =
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "spacetimedb",
|
||||
"message": "操作不合法",
|
||||
}));
|
||||
let other_error = AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "spacetimedb",
|
||||
"message": "陶泥币余额不足",
|
||||
}));
|
||||
|
||||
assert!(should_sync_puzzle_freeze_boundary(&invalid_operation, true));
|
||||
assert!(!should_sync_puzzle_freeze_boundary(
|
||||
&invalid_operation,
|
||||
false
|
||||
));
|
||||
assert!(!should_sync_puzzle_freeze_boundary(&other_error, true));
|
||||
}
|
||||
}
|
||||
|
||||
struct PuzzleDashScopeSettings {
|
||||
|
||||
@@ -29,7 +29,8 @@ use shared_contracts::runtime::{
|
||||
ProfileRechargeOrderResponse, ProfileRechargeProductResponse, ProfileRedeemCodeAdminResponse,
|
||||
ProfileReferralInviteCenterResponse, ProfileWalletLedgerEntryResponse,
|
||||
ProfileWalletLedgerResponse, RedeemProfileReferralInviteCodeRequest,
|
||||
RedeemProfileRewardCodeRequest, RedeemProfileRewardCodeResponse,
|
||||
RedeemProfileReferralInviteCodeResponse, RedeemProfileRewardCodeRequest,
|
||||
RedeemProfileRewardCodeResponse,
|
||||
};
|
||||
use spacetime_client::SpacetimeClientError;
|
||||
use time::OffsetDateTime;
|
||||
@@ -216,14 +217,27 @@ pub async fn get_profile_referral_invite_center(
|
||||
}
|
||||
|
||||
pub async fn redeem_profile_referral_invite_code(
|
||||
State(_state): State<AppState>,
|
||||
State(state): State<AppState>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(_authenticated): Extension<AuthenticatedAccessToken>,
|
||||
Json(_payload): Json<RedeemProfileReferralInviteCodeRequest>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
Json(payload): Json<RedeemProfileReferralInviteCodeRequest>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
Err(runtime_profile_error_response(
|
||||
&request_context,
|
||||
AppError::from_status(StatusCode::BAD_REQUEST).with_message("邀请码仅注册时填写"),
|
||||
let user_id = authenticated.claims().user_id().to_string();
|
||||
let updated_at_micros = OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000;
|
||||
let record = state
|
||||
.spacetime_client()
|
||||
.redeem_profile_referral_invite_code(user_id, payload.invite_code, updated_at_micros as i64)
|
||||
.await
|
||||
.map_err(|error| {
|
||||
runtime_profile_error_response(
|
||||
&request_context,
|
||||
map_runtime_profile_client_error(error),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(json_success_body(
|
||||
Some(&request_context),
|
||||
build_redeem_profile_referral_invite_code_response(record),
|
||||
))
|
||||
}
|
||||
|
||||
@@ -507,6 +521,18 @@ fn build_profile_referral_invite_center_response(
|
||||
}
|
||||
}
|
||||
|
||||
fn build_redeem_profile_referral_invite_code_response(
|
||||
record: module_runtime::RuntimeReferralRedeemRecord,
|
||||
) -> RedeemProfileReferralInviteCodeResponse {
|
||||
RedeemProfileReferralInviteCodeResponse {
|
||||
center: build_profile_referral_invite_center_response(record.center),
|
||||
invitee_reward_granted: record.invitee_reward_granted,
|
||||
inviter_reward_granted: record.inviter_reward_granted,
|
||||
invitee_balance_after: record.invitee_balance_after,
|
||||
inviter_balance_after: record.inviter_balance_after,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_redeem_profile_reward_code_response(
|
||||
record: RuntimeProfileRewardCodeRedeemRecord,
|
||||
) -> RedeemProfileRewardCodeResponse {
|
||||
@@ -603,6 +629,7 @@ mod tests {
|
||||
AccessTokenClaims, AccessTokenClaimsInput, AuthProvider, BindingStatus, sign_access_token,
|
||||
};
|
||||
use serde_json::Value;
|
||||
use std::time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
use tower::ServiceExt;
|
||||
|
||||
@@ -759,7 +786,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn profile_referral_redeem_code_rejects_authenticated_manual_fill() {
|
||||
async fn profile_referral_redeem_code_calls_spacetime_for_authenticated_user() {
|
||||
let state = seed_authenticated_state().await;
|
||||
let token = issue_access_token(&state);
|
||||
let app = build_router(state);
|
||||
@@ -777,7 +804,7 @@ mod tests {
|
||||
.await
|
||||
.expect("request should succeed");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
||||
assert_eq!(response.status(), StatusCode::BAD_GATEWAY);
|
||||
let body = response
|
||||
.into_body()
|
||||
.collect()
|
||||
@@ -787,8 +814,8 @@ mod tests {
|
||||
let payload: Value =
|
||||
serde_json::from_slice(&body).expect("response body should be valid json");
|
||||
assert_eq!(
|
||||
payload["error"]["message"],
|
||||
Value::String("邀请码仅注册时填写".to_string())
|
||||
payload["error"]["details"]["provider"],
|
||||
Value::String("spacetimedb".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -900,7 +927,7 @@ mod tests {
|
||||
}
|
||||
|
||||
async fn seed_authenticated_state() -> AppState {
|
||||
let state = AppState::new(AppConfig::default()).expect("state should build");
|
||||
let state = AppState::new(fast_spacetime_timeout_config()).expect("state should build");
|
||||
state
|
||||
.seed_test_phone_user_with_password("13800138104", "secret123")
|
||||
.await
|
||||
@@ -908,6 +935,13 @@ mod tests {
|
||||
state
|
||||
}
|
||||
|
||||
fn fast_spacetime_timeout_config() -> AppConfig {
|
||||
AppConfig {
|
||||
spacetime_procedure_timeout: Duration::from_secs(1),
|
||||
..AppConfig::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn issue_access_token(state: &AppState) -> String {
|
||||
let claims = AccessTokenClaims::from_input(
|
||||
AccessTokenClaimsInput {
|
||||
|
||||
Reference in New Issue
Block a user