推进 server-rs DDD 分层与新接口接线
This commit is contained in:
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
Reference in New Issue
Block a user