1
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-05-02 20:43:41 +08:00
parent 543ccf2509
commit 5831703156
36 changed files with 799 additions and 254 deletions

View File

@@ -36,11 +36,12 @@ use shared_contracts::{
},
puzzle_gallery::{PuzzleGalleryDetailResponse, PuzzleGalleryResponse},
puzzle_runtime::{
DragPuzzlePieceRequest, PuzzleBoardSnapshotResponse, PuzzleCellPositionResponse,
PuzzleLeaderboardEntryResponse, PuzzleMergedGroupStateResponse, PuzzlePieceStateResponse,
PuzzleRecommendedNextWorkResponse, PuzzleRunResponse, PuzzleRunSnapshotResponse,
PuzzleRuntimeLevelSnapshotResponse, StartPuzzleRunRequest, SubmitPuzzleLeaderboardRequest,
SwapPuzzlePiecesRequest, UpdatePuzzleRuntimePauseRequest, UsePuzzleRuntimePropRequest,
AdvancePuzzleNextLevelRequest, DragPuzzlePieceRequest, PuzzleBoardSnapshotResponse,
PuzzleCellPositionResponse, PuzzleLeaderboardEntryResponse, PuzzleMergedGroupStateResponse,
PuzzlePieceStateResponse, PuzzleRecommendedNextWorkResponse, PuzzleRunResponse,
PuzzleRunSnapshotResponse, PuzzleRuntimeLevelSnapshotResponse, StartPuzzleRunRequest,
SubmitPuzzleLeaderboardRequest, SwapPuzzlePiecesRequest, UpdatePuzzleRuntimePauseRequest,
UsePuzzleRuntimePropRequest,
},
puzzle_works::{
PutPuzzleWorkRequest, PuzzleWorkDetailResponse, PuzzleWorkMutationResponse,
@@ -1367,14 +1368,34 @@ pub async fn advance_puzzle_next_level(
AxumPath(run_id): AxumPath<String>,
Extension(request_context): Extension<RequestContext>,
Extension(authenticated): Extension<AuthenticatedAccessToken>,
payload: Result<Json<AdvancePuzzleNextLevelRequest>, JsonRejection>,
) -> Result<Json<Value>, Response> {
ensure_non_empty(&request_context, PUZZLE_RUNTIME_PROVIDER, &run_id, "runId")?;
let payload = match payload {
Ok(Json(payload)) => payload,
Err(error) if error.status() == StatusCode::UNSUPPORTED_MEDIA_TYPE => {
AdvancePuzzleNextLevelRequest {
target_profile_id: None,
}
}
Err(error) => {
return Err(puzzle_error_response(
&request_context,
PUZZLE_RUNTIME_PROVIDER,
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": PUZZLE_RUNTIME_PROVIDER,
"message": error.body_text(),
})),
));
}
};
let run = state
.spacetime_client()
.advance_puzzle_next_level(spacetime_client::PuzzleRunNextLevelRecordInput {
run_id,
owner_user_id: authenticated.claims().user_id().to_string(),
target_profile_id: payload.target_profile_id,
advanced_at_micros: current_utc_micros(),
})
.await

View File

@@ -227,7 +227,7 @@ pub fn build_system_username(prefix: &str, sequence: u64) -> String {
format!("{prefix}_{sequence:08}")
}
// 公开叙世号是稳定的公开检索键,不替代内部 user_id仅用于展示、分享与搜索。
// 公开百梦号是稳定的公开检索键,不替代内部 user_id仅用于展示、分享与搜索。
pub fn build_public_user_code(sequence: u64) -> String {
format!("SY-{sequence:08}")
}

View File

@@ -67,7 +67,7 @@ impl fmt::Display for PasswordEntryError {
Self::InvalidDisplayName => f.write_str("昵称格式不正确"),
Self::InvalidAvatarDataUrl => f.write_str("头像图片格式不正确"),
Self::EmptyProfileUpdate => f.write_str("请至少修改昵称或头像"),
Self::InvalidPublicUserCode => f.write_str("叙世号格式不正确"),
Self::InvalidPublicUserCode => f.write_str("百梦号格式不正确"),
Self::InvalidCredentials => f.write_str("手机号或密码错误"),
Self::UserNotFound => f.write_str("用户不存在"),
Self::Store(message) | Self::PasswordHash(message) => f.write_str(message),

View File

@@ -1373,8 +1373,8 @@ pub fn advance_to_new_work_first_level_at(
return Err(PuzzleFieldError::InvalidOperation);
}
// 中文注释:跨作品代表进入一个新作品,关卡序号、切割规格和倒计时都从第 1 关重新开始
let next_level_index = 1;
// 中文注释:跨作品只切换到候选作品的第一张图,运行时关卡序号和难度循环继续累进
let next_level_index = run.current_level_index + 1;
let level_config = resolve_puzzle_level_config(next_level_index);
let grid_size = level_config.grid_size;
let shuffle_seed = puzzle_shuffle_seed(
@@ -1391,8 +1391,8 @@ pub fn advance_to_new_work_first_level_at(
Ok(PuzzleRunSnapshot {
run_id: run.run_id.clone(),
entry_profile_id: next_profile.profile_id.clone(),
cleared_level_count: 0,
entry_profile_id: run.entry_profile_id.clone(),
cleared_level_count: run.cleared_level_count,
current_level_index: next_level_index,
current_grid_size: grid_size,
played_profile_ids,
@@ -2998,7 +2998,7 @@ mod tests {
}
#[test]
fn advance_to_new_work_first_level_restarts_level_progress() {
fn advance_to_new_work_first_profile_level_keeps_runtime_progress() {
let first_profile = build_published_profile("entry", "owner-a", vec!["奇幻", "遗迹"]);
let next_profile = build_published_profile("next", "owner-b", vec!["奇幻", "魔法"]);
let mut run = start_run("run-cross-work".to_string(), &first_profile, 2).expect("run");
@@ -3011,14 +3011,14 @@ mod tests {
let next_run =
advance_to_new_work_first_level_at(&run, &next_profile, 3_000).expect("next run");
assert_eq!(next_run.entry_profile_id, "next");
assert_eq!(next_run.cleared_level_count, 0);
assert_eq!(next_run.current_level_index, 1);
assert_eq!(next_run.entry_profile_id, "entry");
assert_eq!(next_run.cleared_level_count, 3);
assert_eq!(next_run.current_level_index, 4);
let next_level = next_run.current_level.expect("next level");
assert_eq!(next_level.profile_id, "next");
assert_eq!(next_level.level_index, 1);
assert_eq!(next_level.grid_size, 3);
assert_eq!(next_level.time_limit_ms, 300_000);
assert_eq!(next_level.level_index, 4);
assert_eq!(next_level.grid_size, 5);
assert_eq!(next_level.time_limit_ms, 210_000);
}
#[test]

View File

@@ -213,6 +213,8 @@ pub struct PuzzleRunDragInput {
pub struct PuzzleRunNextLevelInput {
pub run_id: String,
pub owner_user_id: String,
#[serde(default)]
pub target_profile_id: Option<String>,
pub advanced_at_micros: i64,
}

View File

@@ -75,7 +75,7 @@ impl std::fmt::Display for RuntimeProfileFieldError {
Self::InvalidWalletAmount => f.write_str("profile.wallet_amount 必须大于 0"),
Self::WalletAmountOverflow => f.write_str("profile.wallet_amount 超出上限"),
Self::WalletBalanceOverflow => f.write_str("profile.wallet_balance 超出上限"),
Self::InsufficientWalletBalance => f.write_str("叙世币余额不足"),
Self::InsufficientWalletBalance => f.write_str("光点余额不足"),
Self::MissingInviteCode => f.write_str("referral.invite_code 不能为空"),
Self::MissingRedeemCode => f.write_str("兑换码不能为空"),
Self::RedeemCodeDisabled => f.write_str("兑换码已停用"),

View File

@@ -22,57 +22,57 @@ pub fn runtime_profile_recharge_point_products() -> Vec<RuntimeProfileRechargePr
vec![
build_points_recharge_product(
"points_60",
"60叙世币",
"60光点",
600,
60,
60,
"首充双倍",
"首充送60叙世币",
"首充送60光点",
),
build_points_recharge_product(
"points_180",
"180叙世币",
"180光点",
1800,
180,
180,
"首充双倍",
"首充送180叙世币",
"首充送180光点",
),
build_points_recharge_product(
"points_300",
"300叙世币",
"300光点",
3000,
300,
300,
"首充双倍",
"首充送300叙世币",
"首充送300光点",
),
build_points_recharge_product(
"points_680",
"680叙世币",
"680光点",
6800,
680,
680,
"首充双倍",
"首充送680叙世币",
"首充送680光点",
),
build_points_recharge_product(
"points_1280",
"1280叙世币",
"1280光点",
12800,
1280,
1280,
"首充双倍",
"首充送1280叙世币",
"首充送1280光点",
),
build_points_recharge_product(
"points_3280",
"3280叙世币",
"3280光点",
32800,
3280,
3280,
"首充双倍",
"首充送3280叙世币",
"首充送3280光点",
),
]
}
@@ -121,7 +121,7 @@ pub fn runtime_profile_membership_benefits() -> Vec<RuntimeProfileMembershipBene
year_value: "¥248".to_string(),
},
RuntimeProfileMembershipBenefitSnapshot {
benefit_name: "叙世币回合数".to_string(),
benefit_name: "光点回合数".to_string(),
normal_value: "30".to_string(),
month_value: "100".to_string(),
season_value: "100".to_string(),
@@ -457,14 +457,14 @@ mod tests {
assert_eq!(point_products.len(), 6);
assert_eq!(point_products[0].product_id, "points_60");
assert_eq!(point_products[0].title, "60叙世币");
assert_eq!(point_products[0].title, "60光点");
assert_eq!(point_products[0].price_cents, 600);
assert_eq!(point_products[0].bonus_points, 60);
assert_eq!(point_products[0].description, "首充送60叙世币");
assert_eq!(point_products[0].description, "首充送60光点");
assert_eq!(point_products[5].product_id, "points_3280");
assert_eq!(point_products[5].price_cents, 32800);
assert_eq!(point_products[5].bonus_points, 3280);
assert_eq!(point_products[5].description, "首充送3280叙世币");
assert_eq!(point_products[5].description, "首充送3280光点");
assert_eq!(membership_products.len(), 3);
assert_eq!(membership_products[0].title, "月卡");
assert_eq!(membership_products[0].price_cents, 2800);
@@ -474,7 +474,7 @@ mod tests {
assert!(
benefits
.iter()
.any(|benefit| benefit.benefit_name == "叙世币回合数")
.any(|benefit| benefit.benefit_name == "光点回合数")
);
}

View File

@@ -23,6 +23,13 @@ pub struct DragPuzzlePieceRequest {
pub target_col: u32,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct AdvancePuzzleNextLevelRequest {
#[serde(default)]
pub target_profile_id: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct UsePuzzleRuntimePropRequest {

View File

@@ -4783,6 +4783,7 @@ pub struct PuzzleRunDragRecordInput {
pub struct PuzzleRunNextLevelRecordInput {
pub run_id: String,
pub owner_user_id: String,
pub target_profile_id: Option<String>,
pub advanced_at_micros: i64,
}

View File

@@ -1,7 +1,7 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
// This was generated using spacetimedb cli version 2.1.0 (commit 10a4779b1338eff3708493d87496b51842a7c412).
// This was generated using spacetimedb cli version 2.1.0 (commit 6981f48b4bc1a71c8dd9bdfe5a2c343f6370243d).
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};

View File

@@ -9,6 +9,7 @@ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
pub struct PuzzleRunNextLevelInput {
pub run_id: String,
pub owner_user_id: String,
pub target_profile_id: Option<String>,
pub advanced_at_micros: i64,
}

View File

@@ -559,6 +559,7 @@ impl SpacetimeClient {
let procedure_input = PuzzleRunNextLevelInput {
run_id: input.run_id,
owner_user_id: input.owner_user_id,
target_profile_id: input.target_profile_id,
advanced_at_micros: input.advanced_at_micros,
};

View File

@@ -16,7 +16,7 @@ pub struct CustomWorldProfile {
owner_user_id: String,
// 作品公开编号是稳定分享键,第一次发布时分配,后续重复发布沿用。
public_work_code: Option<String>,
// 作者公开叙世号在发布时固化到作品真相,供广场读模型与搜索结果直接展示。
// 作者公开百梦号在发布时固化到作品真相,供广场读模型与搜索结果直接展示。
author_public_user_code: Option<String>,
source_agent_session_id: Option<String>,
publication_status: CustomWorldPublicationStatus,

View File

@@ -1769,17 +1769,36 @@ fn advance_puzzle_next_level_tx(
let same_work_next_profile =
selected_profile_level_after_runtime_level(&current_profile, current_level)
.map(|level| profile_for_single_level(&current_profile, &level));
let candidates = if same_work_next_profile.is_none() {
list_published_puzzle_profiles(ctx)?
} else {
Vec::new()
};
let similar_work_next_profile = if same_work_next_profile.is_none() {
let candidates = list_published_puzzle_profiles(ctx)?;
select_next_profiles(
let selected_candidates = select_next_profiles(
&current_profile,
&current_run.played_profile_ids,
&candidates,
1,
3,
);
Some(
if let Some(target_profile_id) = input.target_profile_id.as_ref().and_then(|value| {
let trimmed = value.trim();
(!trimmed.is_empty()).then(|| trimmed.to_string())
}) {
selected_candidates
.into_iter()
.find(|candidate| candidate.profile_id == target_profile_id)
.cloned()
.ok_or_else(|| "目标拼图作品不在当前下一关候选中".to_string())?
} else {
selected_candidates
.into_iter()
.next()
.cloned()
.ok_or_else(|| "没有可用的下一关候选".to_string())?
},
)
.into_iter()
.next()
.cloned()
} else {
None
};