推进 server-rs DDD 分层与新接口接线

This commit is contained in:
Codex
2026-04-29 15:46:16 +08:00
parent 9d3fcfae77
commit f82775b852
89 changed files with 3657 additions and 9636 deletions

View File

@@ -1,3 +1,136 @@
//! 大鱼吃小鱼应用编排过渡落位。
//!
//! 这里只组合领域规则并返回结果或事件,不直接调用外部图片、视频或存储服务。
use shared_kernel::normalize_required_string;
use crate::{
BigFishAssetSlotSnapshot, build_asset_coverage,
commands::EvaluateBigFishPublishReadinessCommand, domain::BigFishPublishReadiness,
errors::BigFishApplicationError, events::BigFishDomainEvent,
};
/// 发布门禁应用结果,供 adapter 持久化快照或转换成 API DTO。
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EvaluateBigFishPublishReadinessResult {
pub readiness: BigFishPublishReadiness,
pub events: Vec<BigFishDomainEvent>,
}
/// 评估 Big Fish 作品是否具备发布条件。
///
/// 规则只依赖草稿和资产槽:草稿必须存在,等级主图、基础动作和背景图
/// 必须满足 `build_asset_coverage` 的统一口径。
pub fn evaluate_publish_readiness(
command: EvaluateBigFishPublishReadinessCommand,
asset_slots: &[BigFishAssetSlotSnapshot],
) -> Result<EvaluateBigFishPublishReadinessResult, BigFishApplicationError> {
let session_id = normalize_required_string(command.session_id)
.ok_or(BigFishApplicationError::MissingSessionId)?;
let owner_user_id = normalize_required_string(command.owner_user_id)
.ok_or(BigFishApplicationError::MissingOwnerUserId)?;
let coverage = build_asset_coverage(command.draft.as_ref(), asset_slots);
let readiness = BigFishPublishReadiness {
session_id: session_id.clone(),
owner_user_id: owner_user_id.clone(),
publish_ready: coverage.publish_ready,
blockers: coverage.blockers.clone(),
evaluated_at_micros: command.evaluated_at_micros,
};
let event = BigFishDomainEvent::PublishReadinessEvaluated {
session_id,
owner_user_id,
publish_ready: readiness.publish_ready,
blockers: readiness.blockers.clone(),
occurred_at_micros: readiness.evaluated_at_micros,
};
Ok(EvaluateBigFishPublishReadinessResult {
readiness,
events: vec![event],
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
BigFishAssetKind, build_generated_asset_slot, compile_default_draft, infer_anchor_pack,
};
fn build_command() -> EvaluateBigFishPublishReadinessCommand {
EvaluateBigFishPublishReadinessCommand {
session_id: "big-fish-session-1".to_string(),
owner_user_id: "user-1".to_string(),
draft: Some(compile_default_draft(&infer_anchor_pack("机械深海", None))),
evaluated_at_micros: 1_713_680_000_000_000,
}
}
#[test]
fn evaluate_publish_readiness_reports_blockers_when_assets_missing() {
let result = evaluate_publish_readiness(build_command(), &[]).expect("result");
assert!(!result.readiness.publish_ready);
assert!(
result
.readiness
.blockers
.iter()
.any(|item| item.contains("等级主图"))
);
assert_eq!(result.events.len(), 1);
}
#[test]
fn evaluate_publish_readiness_accepts_complete_assets() {
let command = build_command();
let draft = command.draft.clone().expect("draft");
let mut slots = Vec::new();
for level in 1..=draft.runtime_params.level_count {
slots.push(
build_generated_asset_slot(
&command.session_id,
&draft,
BigFishAssetKind::LevelMainImage,
Some(level),
None,
Some(format!("/assets/level-{level}.png")),
command.evaluated_at_micros + level as i64,
)
.expect("main image slot"),
);
for motion_key in ["idle_float", "move_swim"] {
slots.push(
build_generated_asset_slot(
&command.session_id,
&draft,
BigFishAssetKind::LevelMotion,
Some(level),
Some(motion_key.to_string()),
Some(format!("/assets/level-{level}-{motion_key}.webm")),
command.evaluated_at_micros + 100 + level as i64,
)
.expect("motion slot"),
);
}
}
slots.push(
build_generated_asset_slot(
&command.session_id,
&draft,
BigFishAssetKind::StageBackground,
None,
None,
Some("/assets/bg.png".to_string()),
command.evaluated_at_micros + 1_000,
)
.expect("background slot"),
);
let result = evaluate_publish_readiness(command, &slots).expect("result");
assert!(result.readiness.publish_ready);
assert!(result.readiness.blockers.is_empty());
}
}

View File

@@ -1,3 +1,17 @@
//! 大鱼吃小鱼写入命令过渡落位。
//!
//! 用于表达创建会话、写入消息、更新资产槽和推进运行态等输入。
use crate::BigFishGameDraft;
/// 评估作品是否可以发布的纯领域命令。
///
/// adapter 负责把 SpacetimeDB row 或 HTTP DTO 映射成这里的输入;
/// 命令本身只关心草稿与资产槽这些领域事实。
#[derive(Clone, Debug, PartialEq)]
pub struct EvaluateBigFishPublishReadinessCommand {
pub session_id: String,
pub owner_user_id: String,
pub draft: Option<BigFishGameDraft>,
pub evaluated_at_micros: i64,
}

View File

@@ -2,3 +2,15 @@
//!
//! 后续迁移创作会话、资产槽和运行态聚合时,只保留玩法状态与规则;
//! 图片生成、OSS 与 HTTP handler 均留在 adapter 层。
/// 发布门禁的领域判定结果。
///
/// 这里不保存外部任务状态,只表达当前聚合快照是否满足发布条件。
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BigFishPublishReadiness {
pub session_id: String,
pub owner_user_id: String,
pub publish_ready: bool,
pub blockers: Vec<String>,
pub evaluated_at_micros: i64,
}

View File

@@ -1,3 +1,25 @@
//! 大鱼吃小鱼领域错误过渡落位。
//!
//! 错误只表达玩法规则失败,由 HTTP 和 SpacetimeDB adapter 分别映射展示。
use std::{error::Error, fmt};
/// 大鱼吃小鱼应用服务错误。
///
/// 这里不携带 HTTP status 或 SpacetimeDB 字符串错误,避免领域层泄漏 adapter 语义。
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum BigFishApplicationError {
MissingSessionId,
MissingOwnerUserId,
}
impl fmt::Display for BigFishApplicationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::MissingSessionId => f.write_str("big_fish.session_id 不能为空"),
Self::MissingOwnerUserId => f.write_str("big_fish.owner_user_id 不能为空"),
}
}
}
impl Error for BigFishApplicationError {}

View File

@@ -1,3 +1,18 @@
//! 大鱼吃小鱼领域事件过渡落位。
//!
//! 用于表达草稿变化、资产槽变化和运行态 tick 等事实。
/// 大鱼吃小鱼领域事件。
///
/// 事件只描述已经发生的领域事实,后续由 SpacetimeDB adapter 或 BFF
/// 决定是否持久化、投影或通知前端。
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum BigFishDomainEvent {
PublishReadinessEvaluated {
session_id: String,
owner_user_id: String,
publish_ready: bool,
blockers: Vec<String>,
occurred_at_micros: i64,
},
}

View File

@@ -4,6 +4,12 @@ mod domain;
mod errors;
mod events;
pub use application::{EvaluateBigFishPublishReadinessResult, evaluate_publish_readiness};
pub use commands::EvaluateBigFishPublishReadinessCommand;
pub use domain::BigFishPublishReadiness;
pub use errors::BigFishApplicationError;
pub use events::BigFishDomainEvent;
use std::{error::Error, fmt};
use serde::{Deserialize, Serialize};