合并 origin/master

合入 master 的钱包退款 outbox、拼图后台编译互斥与公开链路更新

保留当前分支外部生成 worker 队列语义,并对齐拼图首图 claim 释放顺序
This commit is contained in:
2026-06-11 23:06:41 +08:00
70 changed files with 3167 additions and 538 deletions

View File

@@ -20,8 +20,8 @@ use crate::match3d::tables::{
match_3_d_work_profile, match3d_agent_message, match3d_agent_session, match3d_runtime_run,
};
use crate::puzzle::{
puzzle_agent_message, puzzle_agent_session, puzzle_event, puzzle_leaderboard_entry,
puzzle_runtime_run, puzzle_work_profile,
puzzle_agent_message, puzzle_agent_session, puzzle_background_compile_task, puzzle_event,
puzzle_leaderboard_entry, puzzle_runtime_run, puzzle_work_profile,
};
use crate::puzzle_clear::tables::{
puzzle_clear_agent_session, puzzle_clear_event, puzzle_clear_runtime_run,
@@ -230,6 +230,7 @@ macro_rules! migration_tables {
asset_entity_binding,
asset_event,
puzzle_agent_session,
puzzle_background_compile_task,
puzzle_agent_message,
puzzle_work_profile,
puzzle_event,

View File

@@ -10,19 +10,20 @@ use module_puzzle::{
PUZZLE_NEXT_LEVEL_MODE_SIMILAR_WORKS, PuzzleAgentMessageFinalizeInput, PuzzleAgentMessageKind,
PuzzleAgentMessageRole, PuzzleAgentMessageSnapshot, PuzzleAgentSessionCreateInput,
PuzzleAgentSessionGetInput, PuzzleAgentSessionProcedureResult, PuzzleAgentSessionSnapshot,
PuzzleAgentStage, PuzzleAnchorPack, PuzzleDraftCompileFailureInput, PuzzleDraftCompileInput,
PuzzleFormDraftSaveInput, PuzzleGeneratedImageCandidate, PuzzleGeneratedImagesSaveInput,
PuzzleLeaderboardEntry, PuzzleLeaderboardSubmitInput, PuzzleLevelGenerationFailureInput,
PuzzlePublicationStatus, PuzzlePublishInput, PuzzleRecommendedNextWork, PuzzleResultDraft,
PuzzleRunDragInput, PuzzleRunGetInput, PuzzleRunNextLevelInput, PuzzleRunPauseInput,
PuzzleRunProcedureResult, PuzzleRunPropInput, PuzzleRunSnapshot, PuzzleRunStartInput,
PuzzleRunSwapInput, PuzzleRuntimeLevelStatus, PuzzleSelectCoverImageInput,
PuzzleUiBackgroundSaveInput, PuzzleWorkDeleteInput, PuzzleWorkGetInput,
PuzzleWorkLikeRecordInput as PuzzleWorkLikeInput, PuzzleWorkPointIncentiveClaimInput,
PuzzleWorkProcedureResult, PuzzleWorkProfile, PuzzleWorkRemixInput, PuzzleWorkUpsertInput,
PuzzleWorksListInput, PuzzleWorksProcedureResult, apply_publish_overrides_to_draft,
apply_selected_candidate, build_form_draft_from_seed, build_result_preview,
compile_result_draft_from_seed, create_work_profile, infer_anchor_pack,
PuzzleAgentStage, PuzzleAnchorPack, PuzzleBackgroundCompileTaskClaimInput,
PuzzleBackgroundCompileTaskProcedureResult, PuzzleBackgroundCompileTaskReleaseInput,
PuzzleDraftCompileFailureInput, PuzzleDraftCompileInput, PuzzleFormDraftSaveInput,
PuzzleGeneratedImageCandidate, PuzzleGeneratedImagesSaveInput, PuzzleLeaderboardEntry,
PuzzleLeaderboardSubmitInput, PuzzleLevelGenerationFailureInput, PuzzlePublicationStatus,
PuzzlePublishInput, PuzzleRecommendedNextWork, PuzzleResultDraft, PuzzleRunDragInput,
PuzzleRunGetInput, PuzzleRunNextLevelInput, PuzzleRunPauseInput, PuzzleRunProcedureResult,
PuzzleRunPropInput, PuzzleRunSnapshot, PuzzleRunStartInput, PuzzleRunSwapInput,
PuzzleRuntimeLevelStatus, PuzzleSelectCoverImageInput, PuzzleUiBackgroundSaveInput,
PuzzleWorkDeleteInput, PuzzleWorkGetInput, PuzzleWorkLikeRecordInput as PuzzleWorkLikeInput,
PuzzleWorkPointIncentiveClaimInput, PuzzleWorkProcedureResult, PuzzleWorkProfile,
PuzzleWorkRemixInput, PuzzleWorkUpsertInput, PuzzleWorksListInput, PuzzleWorksProcedureResult,
apply_publish_overrides_to_draft, apply_selected_candidate, build_form_draft_from_seed,
build_result_preview, compile_result_draft_from_seed, create_work_profile, infer_anchor_pack,
mark_failed_puzzle_result_draft_generation, normalize_puzzle_draft, normalize_puzzle_levels,
normalize_theme_tags, publish_work_profile, replace_puzzle_level, select_next_profiles,
selected_profile_level_after_runtime_level, selected_puzzle_level, tag_similarity_score,
@@ -40,6 +41,7 @@ use crate::auth::user_account;
use crate::validate_external_generation_job_lease_for_tx;
const PUZZLE_POINT_INCENTIVE_DEFAULT_U64: u64 = 0;
const PUZZLE_BACKGROUND_COMPILE_TASK_LEASE_MICROS: i64 = 30 * 60 * 1_000_000;
const WORK_VISIBLE_DEFAULT: bool = true;
const PUZZLE_EXTERNAL_GENERATION_SOURCE_MODULE: &str = "puzzle";
const PUZZLE_COMPILE_DRAFT_JOB_KIND: &str = "puzzle_compile_draft";
@@ -68,6 +70,22 @@ pub struct PuzzleAgentSessionRow {
updated_at: Timestamp,
}
/// 拼图首图后台编译活动任务表。
/// 中文注释:该表只保存跨 api-server 实例互斥 claim不表达最终生成结果。
#[spacetimedb::table(
accessor = puzzle_background_compile_task,
index(accessor = by_puzzle_background_compile_task_session_id, btree(columns = [session_id]))
)]
pub struct PuzzleBackgroundCompileTaskRow {
#[primary_key]
task_id: String,
claim_id: String,
session_id: String,
owner_user_id: String,
created_at: Timestamp,
updated_at: Timestamp,
}
/// 拼图 Agent 消息真相表。
#[spacetimedb::table(
accessor = puzzle_agent_message,
@@ -413,6 +431,43 @@ pub fn mark_puzzle_level_generation_failed(
}
}
#[spacetimedb::procedure]
pub fn claim_puzzle_background_compile_task(
ctx: &mut ProcedureContext,
input: PuzzleBackgroundCompileTaskClaimInput,
) -> PuzzleBackgroundCompileTaskProcedureResult {
match ctx.try_with_tx(|tx| claim_puzzle_background_compile_task_tx(tx, input.clone())) {
Ok(claimed) => PuzzleBackgroundCompileTaskProcedureResult {
ok: true,
claimed,
error_message: None,
},
Err(message) => PuzzleBackgroundCompileTaskProcedureResult {
ok: false,
claimed: false,
error_message: Some(message),
},
}
}
#[spacetimedb::procedure]
pub fn release_puzzle_background_compile_task(
ctx: &mut ProcedureContext,
input: PuzzleBackgroundCompileTaskReleaseInput,
) -> PuzzleBackgroundCompileTaskProcedureResult {
match ctx.try_with_tx(|tx| release_puzzle_background_compile_task_tx(tx, input.clone())) {
Ok(released) => PuzzleBackgroundCompileTaskProcedureResult {
ok: true,
claimed: released,
error_message: None,
},
Err(message) => PuzzleBackgroundCompileTaskProcedureResult {
ok: false,
claimed: false,
error_message: Some(message),
},
}
}
/// 保存拼图入口表单草稿。
/// 中文注释:该 procedure 只更新 session 与创作中心草稿卡,不触发图片生成或发布校验。
#[spacetimedb::procedure]
@@ -1060,6 +1115,84 @@ fn compile_puzzle_agent_draft_tx(
)
}
fn claim_puzzle_background_compile_task_tx(
ctx: &TxContext,
input: PuzzleBackgroundCompileTaskClaimInput,
) -> Result<bool, String> {
let task_id = normalize_required_puzzle_task_field(&input.task_id, "拼图后台任务 ID")?;
let claim_id = normalize_required_puzzle_task_field(&input.claim_id, "拼图后台任务 claim ID")?;
let session_id = normalize_required_puzzle_task_field(&input.session_id, "拼图 session ID")?;
let owner_user_id = normalize_required_puzzle_task_field(&input.owner_user_id, "拼图用户 ID")?;
let claimed_at = Timestamp::from_micros_since_unix_epoch(input.claimed_at_micros);
get_owned_session_row(ctx, &session_id, &owner_user_id)?;
if let Some(existing) = ctx
.db
.puzzle_background_compile_task()
.task_id()
.find(&task_id)
{
if !is_stale_puzzle_background_compile_task(&existing, input.claimed_at_micros) {
return Ok(false);
}
ctx.db
.puzzle_background_compile_task()
.task_id()
.delete(&task_id);
}
ctx.db
.puzzle_background_compile_task()
.insert(PuzzleBackgroundCompileTaskRow {
task_id,
claim_id,
session_id,
owner_user_id,
created_at: claimed_at,
updated_at: claimed_at,
});
Ok(true)
}
fn release_puzzle_background_compile_task_tx(
ctx: &TxContext,
input: PuzzleBackgroundCompileTaskReleaseInput,
) -> Result<bool, String> {
let task_id = normalize_required_puzzle_task_field(&input.task_id, "拼图后台任务 ID")?;
let claim_id = normalize_required_puzzle_task_field(&input.claim_id, "拼图后台任务 claim ID")?;
let session_id = normalize_required_puzzle_task_field(&input.session_id, "拼图 session ID")?;
let owner_user_id = normalize_required_puzzle_task_field(&input.owner_user_id, "拼图用户 ID")?;
let Some(row) = ctx
.db
.puzzle_background_compile_task()
.task_id()
.find(&task_id)
else {
return Ok(false);
};
if row.session_id != session_id || row.owner_user_id != owner_user_id {
return Err("无权释放该拼图后台任务".to_string());
}
if row.claim_id != claim_id {
return Ok(false);
}
ctx.db
.puzzle_background_compile_task()
.task_id()
.delete(&task_id);
Ok(true)
}
fn is_stale_puzzle_background_compile_task(
row: &PuzzleBackgroundCompileTaskRow,
now_micros: i64,
) -> bool {
now_micros.saturating_sub(row.updated_at.to_micros_since_unix_epoch())
>= PUZZLE_BACKGROUND_COMPILE_TASK_LEASE_MICROS
}
fn mark_puzzle_draft_generation_failed_tx(
ctx: &TxContext,
input: PuzzleDraftCompileFailureInput,
@@ -3162,6 +3295,14 @@ fn get_owned_session_row(
Ok(row)
}
fn normalize_required_puzzle_task_field(value: &str, field_name: &str) -> Result<String, String> {
let normalized = value.trim();
if normalized.is_empty() {
return Err(format!("{field_name} 不能为空"));
}
Ok(normalized.to_string())
}
fn get_owned_run_row(
ctx: &TxContext,
run_id: &str,