This commit is contained in:
@@ -37,3 +37,4 @@ axum-macros = "0.4"
|
|||||||
axum-test = "14"
|
axum-test = "14"
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
tower = { version = "0.5", features = ["util"] }
|
tower = { version = "0.5", features = ["util"] }
|
||||||
|
tempfile = "3"
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
-- Durable replay and non-repeatable-completion guards for atomic SBC submissions.
|
||||||
|
-- Existing successful rows are treated as non-repeatable; if historical data already
|
||||||
|
-- violates that invariant the migration fails rather than silently discarding history.
|
||||||
|
ALTER TABLE sbc_submissions ADD COLUMN repeatable INTEGER NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX idx_sbc_nonrepeatable_completion
|
||||||
|
ON sbc_submissions(profile_id, sbc_id)
|
||||||
|
WHERE passed = 1 AND repeatable = 0;
|
||||||
|
|
||||||
|
-- submitted_card_ids is stored in canonical sorted order by the writer. This rejects
|
||||||
|
-- stale retries of the same card set even for explicitly repeatable challenges.
|
||||||
|
CREATE UNIQUE INDEX idx_sbc_submission_replay
|
||||||
|
ON sbc_submissions(profile_id, sbc_id, submitted_card_ids)
|
||||||
|
WHERE passed = 1;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- Core owns durable per-profile working squads for SBC challenges. The FIFA17
|
||||||
|
-- adapter maps its numeric challenge id to the opaque generic sbc_id.
|
||||||
|
CREATE TABLE sbc_challenge_squads (
|
||||||
|
profile_id TEXT NOT NULL REFERENCES profiles(id),
|
||||||
|
sbc_id TEXT NOT NULL,
|
||||||
|
owned_card_ids TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (profile_id, sbc_id)
|
||||||
|
);
|
||||||
@@ -251,8 +251,13 @@ pub async fn build(pool: Pool, cfg: Config) -> Result<Router> {
|
|||||||
.route("/matches/opponent", get(routes::matches::get_opponent))
|
.route("/matches/opponent", get(routes::matches::get_opponent))
|
||||||
.route("/matches/result", post(routes::matches::post_match_result))
|
.route("/matches/result", post(routes::matches::post_match_result))
|
||||||
.route("/sbc", get(routes::sbc::get_sbcs))
|
.route("/sbc", get(routes::sbc::get_sbcs))
|
||||||
|
.route("/sbc/status", get(routes::sbc::get_sbc_status))
|
||||||
.route("/sbc/submit", post(routes::sbc::post_sbc_submit))
|
.route("/sbc/submit", post(routes::sbc::post_sbc_submit))
|
||||||
.route("/sbc/:sbc_id", get(routes::sbc::get_sbc))
|
.route("/sbc/:sbc_id", get(routes::sbc::get_sbc))
|
||||||
|
.route(
|
||||||
|
"/sbc/:sbc_id/squad",
|
||||||
|
get(routes::sbc::get_sbc_squad).put(routes::sbc::put_sbc_squad),
|
||||||
|
)
|
||||||
.route("/market", get(routes::market::get_market))
|
.route("/market", get(routes::market::get_market))
|
||||||
.route("/market/buy", post(routes::market::post_market_buy))
|
.route("/market/buy", post(routes::market::post_market_buy))
|
||||||
.route("/market/sell", post(routes::market::post_market_sell))
|
.route("/market/sell", post(routes::market::post_market_sell))
|
||||||
|
|||||||
+14
-2
@@ -33,16 +33,17 @@ pub struct SbcReward {
|
|||||||
pub pack_id: Option<String>,
|
pub pack_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// DB record of a completed submission
|
/// DB record of a completed submission.
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
|
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
|
||||||
pub struct SbcSubmission {
|
pub struct SbcSubmission {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub profile_id: String,
|
pub profile_id: String,
|
||||||
|
pub club_id: Option<String>,
|
||||||
pub sbc_id: String,
|
pub sbc_id: String,
|
||||||
pub submitted_card_ids: String,
|
pub submitted_card_ids: String,
|
||||||
pub passed: bool,
|
pub passed: bool,
|
||||||
pub submitted_at: String,
|
pub submitted_at: String,
|
||||||
|
pub repeatable: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
@@ -51,6 +52,17 @@ pub struct SubmitSbcRequest {
|
|||||||
pub owned_card_ids: Vec<String>,
|
pub owned_card_ids: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct SaveSbcSquadRequest {
|
||||||
|
pub owned_card_ids: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct SbcSquadState {
|
||||||
|
pub sbc_id: String,
|
||||||
|
pub owned_card_ids: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
pub struct SbcResult {
|
pub struct SbcResult {
|
||||||
pub passed: bool,
|
pub passed: bool,
|
||||||
|
|||||||
+52
-12
@@ -8,7 +8,7 @@ use serde_json::{json, Value};
|
|||||||
use crate::{
|
use crate::{
|
||||||
app::AppState,
|
app::AppState,
|
||||||
error::{AppError, AppResult},
|
error::{AppError, AppResult},
|
||||||
models::sbc::{SbcResult, SubmitSbcRequest},
|
models::sbc::{SaveSbcSquadRequest, SbcResult, SubmitSbcRequest},
|
||||||
services::{club as club_svc, profile as profile_svc, sbc as sbc_svc},
|
services::{club as club_svc, profile as profile_svc, sbc as sbc_svc},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -28,6 +28,49 @@ pub async fn get_sbc(
|
|||||||
Ok(Json(json!({ "sbc": sbc })))
|
Ok(Json(json!({ "sbc": sbc })))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_sbc_status(State(state): State<AppState>, game: GameId) -> AppResult<Json<Value>> {
|
||||||
|
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
|
||||||
|
let completions = sbc_svc::completion_counts(&state.pool, &profile.id).await?;
|
||||||
|
Ok(Json(json!({ "completions": completions })))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_sbc_squad(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
game: GameId,
|
||||||
|
Path(sbc_id): Path<String>,
|
||||||
|
) -> AppResult<Json<Value>> {
|
||||||
|
if !state
|
||||||
|
.sbc_defs
|
||||||
|
.iter()
|
||||||
|
.any(|definition| definition.id == sbc_id)
|
||||||
|
{
|
||||||
|
return Err(AppError::NotFound(format!("SBC '{sbc_id}' not found")));
|
||||||
|
}
|
||||||
|
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
|
||||||
|
let squad = sbc_svc::load_sbc_squad(&state.pool, &profile.id, &sbc_id).await?;
|
||||||
|
Ok(Json(json!({ "squad": squad })))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn put_sbc_squad(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
game: GameId,
|
||||||
|
Path(sbc_id): Path<String>,
|
||||||
|
Json(req): Json<SaveSbcSquadRequest>,
|
||||||
|
) -> AppResult<Json<Value>> {
|
||||||
|
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
|
||||||
|
let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
|
||||||
|
let squad = sbc_svc::save_sbc_squad(
|
||||||
|
&state.pool,
|
||||||
|
&state.sbc_defs,
|
||||||
|
&profile.id,
|
||||||
|
&club.id,
|
||||||
|
&sbc_id,
|
||||||
|
&req.owned_card_ids,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(Json(json!({ "squad": squad })))
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn post_sbc_submit(
|
pub async fn post_sbc_submit(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
game: GameId,
|
game: GameId,
|
||||||
@@ -38,20 +81,17 @@ pub async fn post_sbc_submit(
|
|||||||
|
|
||||||
let result = sbc_svc::submit_sbc(
|
let result = sbc_svc::submit_sbc(
|
||||||
&state.pool,
|
&state.pool,
|
||||||
&state.card_db,
|
sbc_svc::SbcSubmissionContext {
|
||||||
&state.sbc_defs,
|
card_db: &state.card_db,
|
||||||
&state.obj_defs,
|
sbc_defs: &state.sbc_defs,
|
||||||
&profile.id,
|
objective_defs: &state.obj_defs,
|
||||||
&club.id,
|
achievement_defs: &state.achievement_defs,
|
||||||
|
profile_id: &profile.id,
|
||||||
|
club_id: &club.id,
|
||||||
|
},
|
||||||
&req,
|
&req,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if result.passed {
|
|
||||||
let _ = crate::services::achievement::check_and_unlock(
|
|
||||||
&state.pool, &state.achievement_defs, &profile.id, &club.id,
|
|
||||||
).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Json(result))
|
Ok(Json(result))
|
||||||
}
|
}
|
||||||
|
|||||||
+1152
-54
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user