Initial commit: OpenFUT Core

Offline Ultimate Team backend — game-independent REST API.

- 19 API endpoints: auth, profiles, clubs, cards, packs, squads,
  objectives, SBCs, match rewards, NPC market, statistics
- Axum + SQLite + SQLx with full migrations
- Weighted pack generator, SBC validation engine
- JSON-driven mod data (cards, packs, objectives, SBCs)
- 5 integration tests passing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-06-25 14:52:06 -07:00
commit 1ffe0ffa9f
60 changed files with 6152 additions and 0 deletions
+93
View File
@@ -0,0 +1,93 @@
use crate::{
db::Pool,
error::{AppError, AppResult},
models::squad::{SaveSquadRequest, Squad, SquadPlayer},
};
use uuid::Uuid;
pub async fn get_squad(pool: &Pool, club_id: &str) -> AppResult<(Squad, Vec<SquadPlayer>)> {
let squad = sqlx::query_as::<_, Squad>(
"SELECT id, club_id, name, formation, created_at, updated_at FROM squads WHERE club_id = ? ORDER BY created_at DESC LIMIT 1"
)
.bind(club_id)
.fetch_optional(pool)
.await?
.ok_or_else(|| AppError::NotFound("no squad found for this club".into()))?;
let players = sqlx::query_as::<_, SquadPlayer>(
"SELECT id, squad_id, owned_card_id, position_index, is_captain, is_on_bench FROM squad_players WHERE squad_id = ?"
)
.bind(&squad.id)
.fetch_all(pool)
.await?;
Ok((squad, players))
}
pub async fn save_squad(pool: &Pool, club_id: &str, req: &SaveSquadRequest) -> AppResult<Squad> {
let now = chrono::Utc::now().to_rfc3339();
let existing = sqlx::query_scalar::<_, Option<String>>(
"SELECT id FROM squads WHERE club_id = ? ORDER BY created_at DESC LIMIT 1",
)
.bind(club_id)
.fetch_one(pool)
.await?;
let squad_id = if let Some(id) = existing {
sqlx::query("UPDATE squads SET name = COALESCE(?, name), formation = COALESCE(?, formation), updated_at = ? WHERE id = ?")
.bind(req.name.as_deref())
.bind(req.formation.as_deref())
.bind(&now)
.bind(&id)
.execute(pool)
.await?;
sqlx::query("DELETE FROM squad_players WHERE squad_id = ?")
.bind(&id)
.execute(pool)
.await?;
id
} else {
let squad = Squad::new(
club_id,
req.name.as_deref().unwrap_or("My Squad"),
req.formation.as_deref().unwrap_or("4-4-2"),
);
sqlx::query(
"INSERT INTO squads (id, club_id, name, formation, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)"
)
.bind(&squad.id)
.bind(&squad.club_id)
.bind(&squad.name)
.bind(&squad.formation)
.bind(&squad.created_at)
.bind(&squad.updated_at)
.execute(pool)
.await?;
squad.id
};
for player in &req.players {
let sp_id = Uuid::new_v4().to_string();
sqlx::query(
"INSERT INTO squad_players (id, squad_id, owned_card_id, position_index, is_captain, is_on_bench) VALUES (?, ?, ?, ?, ?, ?)"
)
.bind(&sp_id)
.bind(&squad_id)
.bind(&player.owned_card_id)
.bind(player.position_index)
.bind(player.is_captain)
.bind(player.is_on_bench)
.execute(pool)
.await?;
}
let squad = sqlx::query_as::<_, Squad>(
"SELECT id, club_id, name, formation, created_at, updated_at FROM squads WHERE id = ?",
)
.bind(&squad_id)
.fetch_one(pool)
.await?;
Ok(squad)
}