chore: establish development baseline

This commit is contained in:
2026-06-28 01:27:09 +08:00
commit dd53e3054e
131 changed files with 19327 additions and 0 deletions
+18
View File
@@ -0,0 +1,18 @@
[package]
name = "bat-core"
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
[dependencies]
anyhow.workspace = true
thiserror.workspace = true
serde.workspace = true
serde_json.workspace = true
async-trait.workspace = true
tokio.workspace = true
[dev-dependencies]
tokio = { workspace = true, features = ["test-util", "macros"] }
tempfile = "3.14"
+167
View File
@@ -0,0 +1,167 @@
//! 游戏客户端领域对象
use std::path::PathBuf;
/// 游戏区域
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub enum GameRegion {
/// 日本服
Japan,
/// 国际服
Global,
/// 韩国服
Korea,
/// 国服
China,
}
impl GameRegion {
/// 获取区域代码
pub fn code(&self) -> &'static str {
match self {
Self::Japan => "JP",
Self::Global => "GLOBAL",
Self::Korea => "KR",
Self::China => "CN",
}
}
}
/// 客户端状态
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum ClientStatus {
/// 原始状态(未修改)
Pristine,
/// 已应用翻译
Translated,
/// 损坏(需要修复)
Corrupted,
/// 未知状态
Unknown,
}
/// 游戏客户端
///
/// 代表用户本地安装的 Blue Archive 游戏客户端
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct GameClient {
/// 安装路径
pub install_path: PathBuf,
/// 游戏区域
pub region: GameRegion,
/// 当前状态
pub status: ClientStatus,
}
impl GameClient {
/// 创建新的游戏客户端实例
///
/// # 参数
/// - `install_path`: 游戏安装目录
/// - `region`: 游戏区域
pub fn new(install_path: PathBuf, region: GameRegion) -> Self {
Self {
install_path,
region,
status: ClientStatus::Unknown,
}
}
/// 发现本地安装的客户端
///
/// # 返回
/// - 成功:返回找到的所有客户端
/// - 失败:返回错误
///
/// # 注意
/// 此功能将在 Phase 3 实现
pub fn discover() -> crate::Result<Vec<GameClient>> {
Err(crate::Error::NotImplemented(
"客户端发现功能将在 Phase 3 实现".to_string()
))
}
/// 验证客户端完整性
///
/// # 返回
/// - true: 客户端完整
/// - false: 客户端损坏
///
/// # 注意
/// 此功能将在 Phase 3 实现
pub fn verify_integrity(&self) -> crate::Result<bool> {
Err(crate::Error::NotImplemented(
"完整性验证将在 Phase 3 实现".to_string()
))
}
/// 获取 StreamingAssets 目录路径
pub fn streaming_assets_path(&self) -> PathBuf {
self.install_path
.join("BlueArchive_Data")
.join("StreamingAssets")
}
/// 获取 AssetBundles 目录路径
pub fn asset_bundles_path(&self) -> PathBuf {
self.streaming_assets_path().join("AssetBundles")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_game_region_code() {
assert_eq!(GameRegion::Japan.code(), "JP");
assert_eq!(GameRegion::Global.code(), "GLOBAL");
assert_eq!(GameRegion::Korea.code(), "KR");
assert_eq!(GameRegion::China.code(), "CN");
}
#[test]
fn test_new_game_client() {
let path = PathBuf::from("/test/path");
let client = GameClient::new(path.clone(), GameRegion::Japan);
assert_eq!(client.install_path, path);
assert_eq!(client.region, GameRegion::Japan);
assert_eq!(client.status, ClientStatus::Unknown);
}
#[test]
fn test_streaming_assets_path() {
let client = GameClient::new(
PathBuf::from("/game/BlueArchive_JP"),
GameRegion::Japan
);
assert_eq!(
client.streaming_assets_path(),
PathBuf::from("/game/BlueArchive_JP/BlueArchive_Data/StreamingAssets")
);
}
#[test]
fn test_asset_bundles_path() {
let client = GameClient::new(
PathBuf::from("/game/BlueArchive_JP"),
GameRegion::Japan
);
assert_eq!(
client.asset_bundles_path(),
PathBuf::from("/game/BlueArchive_JP/BlueArchive_Data/StreamingAssets/AssetBundles")
);
}
#[test]
fn test_discover_not_implemented() {
let result = GameClient::discover();
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), crate::Error::NotImplemented(_)));
}
}
+73
View File
@@ -0,0 +1,73 @@
//! 游戏版本领域对象
use std::fmt;
/// Unity 版本
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct UnityVersion {
/// 主版本号
pub year: u32,
/// 次版本号
pub stream: u32,
/// 补丁版本号
pub patch: u32,
/// 修订版本(如 f2, b1
pub revision: String,
}
impl UnityVersion {
/// 创建新的 Unity 版本
pub fn new(year: u32, stream: u32, patch: u32, revision: String) -> Self {
Self { year, stream, patch, revision }
}
}
impl fmt::Display for UnityVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.{}.{}{}", self.year, self.stream, self.patch, self.revision)
}
}
/// 游戏版本
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct GameVersion {
/// 主版本号
pub major: u32,
/// 次版本号
pub minor: u32,
/// 补丁版本号
pub patch: u32,
/// Unity 版本
pub unity_version: UnityVersion,
}
impl GameVersion {
/// 创建新的游戏版本
pub fn new(major: u32, minor: u32, patch: u32, unity_version: UnityVersion) -> Self {
Self { major, minor, patch, unity_version }
}
}
impl fmt::Display for GameVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_unity_version_display() {
let version = UnityVersion::new(2021, 3, 56, "f2".to_string());
assert_eq!(version.to_string(), "2021.3.56f2");
}
#[test]
fn test_game_version_display() {
let unity = UnityVersion::new(2021, 3, 56, "f2".to_string());
let version = GameVersion::new(1, 70, 0, unity);
assert_eq!(version.to_string(), "1.70.0");
}
}
+11
View File
@@ -0,0 +1,11 @@
//! 领域对象模块
pub mod game_client;
pub mod game_version;
pub mod resource;
pub mod translation;
pub use game_client::{ClientStatus, GameClient, GameRegion};
pub use game_version::{GameVersion, UnityVersion};
pub use resource::{Resource, ResourceEntry, ResourceType};
pub use translation::{ExtractedText, SourceText, TextContext, TextMetadata, TextSource, TranslatedText, TranslationStatus};
+58
View File
@@ -0,0 +1,58 @@
//! 资源领域对象
use std::path::PathBuf;
/// 资源类型
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum ResourceType {
/// AssetBundle
AssetBundle,
/// Manifest
Manifest,
/// TableBundle
TableBundle,
/// 其他
Other,
}
/// 资源条目
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ResourceEntry {
/// 资源路径
pub path: String,
/// 资源 Hash
pub hash: String,
/// 资源大小
pub size: u64,
/// 资源类型
pub resource_type: ResourceType,
}
/// 资源
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Resource {
/// 资源 ID
pub id: String,
/// 本地路径
pub local_path: PathBuf,
/// 资源条目
pub entry: ResourceEntry,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_resource_entry() {
let entry = ResourceEntry {
path: "test.bundle".to_string(),
hash: "abc123".to_string(),
size: 1024,
resource_type: ResourceType::AssetBundle,
};
assert_eq!(entry.path, "test.bundle");
assert_eq!(entry.size, 1024);
}
}
+102
View File
@@ -0,0 +1,102 @@
//! 翻译领域对象
use std::collections::HashMap;
/// 文本来源
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct TextSource {
/// AssetBundle 名称
pub asset_bundle: String,
/// Asset 路径
pub asset_path: String,
/// 对象类型
pub object_type: String,
/// 字段路径
pub field_path: Vec<String>,
}
/// 文本上下文
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct TextContext {
/// 场景
pub scene: Option<String>,
/// 角色
pub character: Option<String>,
/// 额外信息
pub extra: HashMap<String, String>,
}
/// 文本元数据
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct TextMetadata {
/// 标签
pub tags: Vec<String>,
/// 优先级
pub priority: u8,
/// 备注
pub notes: Option<String>,
}
/// 提取的源文本
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ExtractedText {
/// 文本 ID
pub id: String,
/// 来源
pub source: TextSource,
/// 上下文
pub context: TextContext,
/// 内容
pub content: String,
/// 元数据
pub metadata: TextMetadata,
}
/// 源文本
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct SourceText {
/// 文本 ID
pub id: String,
/// 内容
pub content: String,
}
/// 翻译状态
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum TranslationStatus {
/// 待翻译
Pending,
/// 已翻译
Translated,
/// 已审核
Reviewed,
}
/// 翻译后的文本
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct TranslatedText {
/// 源文本 ID
pub source_id: String,
/// 翻译内容
pub translation: String,
/// 提供商
pub provider: String,
/// 置信度
pub confidence: f32,
/// 状态
pub status: TranslationStatus,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_source_text() {
let text = SourceText {
id: "test_001".to_string(),
content: "Hello".to_string(),
};
assert_eq!(text.id, "test_001");
}
}
+34
View File
@@ -0,0 +1,34 @@
//! 错误类型定义
use thiserror::Error;
/// Core 错误类型
#[derive(Error, Debug)]
pub enum Error {
/// 对象不存在
#[error("Object not found: {0}")]
NotFound(String),
/// 无效的参数
#[error("Invalid argument: {0}")]
InvalidArgument(String),
/// 功能尚未实现
#[error("Not implemented: {0}")]
NotImplemented(String),
/// IO 错误
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
/// 序列化错误
#[error("Serialization error: {0}")]
Serialization(String),
/// 其他错误
#[error(transparent)]
Other(#[from] anyhow::Error),
}
/// Core Result 类型
pub type Result<T> = std::result::Result<T, Error>;
+36
View File
@@ -0,0 +1,36 @@
//! # BAT Core - 核心领域层
//!
//! BlueArchive Toolkit 的核心领域模型和业务逻辑
//!
//! 本模块包含:
//! - 领域对象(Domain Objects
//! - 仓储接口(Repository Interfaces
//! - 领域服务(Domain Services
//!
//! 设计原则:
//! - 领域层不依赖任何外部技术细节
//! - 所有依赖通过接口注入
//! - 纯粹的业务逻辑
#![warn(missing_docs)]
#![warn(clippy::all)]
pub mod domain;
pub mod error;
pub mod repositories;
pub mod services;
pub use error::{Error, Result};
/// Core 版本号
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_version() {
assert!(!VERSION.is_empty());
}
}
+363
View File
@@ -0,0 +1,363 @@
//! CAS 仓储接口
//!
//! 定义内容寻址存储(Content-Addressable Storage)的统一接口。
//!
//! # 设计原则
//!
//! 1. **原子性**:所有操作保证原子性,要么全部成功,要么全部失败
//! 2. **一致性**:引用计数与对象存储保持强一致性
//! 3. **幂等性**:相同内容多次存储返回相同结果
//! 4. **完整性**:所有操作自动验证 Hash 完整性
//!
//! # 使用场景
//!
//! - AssetBundle 去重存储
//! - 翻译资源缓存
//! - 临时文件管理
//!
//! # 示例
//!
//! ```rust,ignore
//! use bat_core::repositories::CasRepository;
//!
//! // 存储对象
//! let data = b"Hello, World!";
//! let object_id = repository.store(data).await?;
//!
//! // 获取对象
//! let retrieved = repository.get(&object_id).await?;
//! assert_eq!(data, retrieved.as_slice());
//!
//! // 管理引用
//! repository.add_reference(&object_id).await?;
//! repository.remove_reference(&object_id).await?;
//! ```
use async_trait::async_trait;
use std::path::Path;
/// 对象 ID(基于内容 Hash
///
/// 对象 ID 是内容的唯一标识符,由内容的 Hash 值生成。
/// 相同内容总是产生相同的对象 ID。
pub type ObjectId = String;
/// CAS 仓储接口
///
/// 提供统一的内容寻址存储访问接口,支持:
/// - 存储和检索:基于内容 Hash 的对象存储
/// - 引用计数管理:自动追踪对象的引用关系
/// - 完整性校验:所有操作自动验证 Hash
/// - 事务支持:原子性操作保证
///
/// # 实现要求
///
/// 实现此接口时必须保证:
/// - 线程安全:所有方法都是 `Send + Sync`
/// - 原子性:`store()` 操作必须原子完成存储和引用计数
/// - 一致性:引用计数与实际对象保持一致
/// - 幂等性:相同输入多次调用产生相同结果
///
/// # 错误处理
///
/// 所有方法返回 `Result`,可能的错误包括:
/// - `Error::Io`I/O 错误
/// - `Error::NotFound`:对象不存在
/// - `Error::InvalidArgument`:参数无效(如空 ID
#[async_trait]
pub trait CasRepository: Send + Sync {
/// 存储对象
///
/// 将数据存储到 CAS 中,并返回对象 ID。如果相同内容已存在,
/// 则直接返回现有对象的 ID 并增加引用计数。
///
/// # 参数
///
/// - `data`: 要存储的数据(任意字节流)
///
/// # 返回
///
/// - 成功:返回对象 ID(基于内容 Hash)
/// - 失败:返回错误(如 I/O 错误)
///
/// # 保证
///
/// - **原子性**:存储和引用计数增加是原子的
/// - **幂等性**:相同内容多次存储返回相同 ID
/// - **完整性**:自动计算和验证 Hash
///
/// # 示例
///
/// ```rust,ignore
/// let data = b"Hello, World!";
/// let id = repository.store(data).await?;
/// println!("Stored with ID: {}", id);
/// ```
///
/// # 实现注意事项
///
/// - 如果对象已存在,只需增加引用计数
/// - Hash 算法应使用 BLAKE3(快速且安全)
/// - 存储路径建议:`objects/{hash[0..2]}/{hash[2..4]}/{hash}`
async fn store(&self, data: &[u8]) -> crate::Result<ObjectId>;
/// 获取对象
///
/// 根据对象 ID 获取对象数据。
///
/// # 参数
///
/// - `id`: 对象 ID
///
/// # 返回
///
/// - 成功:返回对象数据
/// - 失败:如果对象不存在返回 `Error::NotFound`
///
/// # 保证
///
/// - **完整性校验**:返回前验证 Hash 是否匹配
/// - **不变性**:对象内容永不改变
///
/// # 示例
///
/// ```rust,ignore
/// let data = repository.get(&object_id).await?;
/// println!("Retrieved {} bytes", data.len());
/// ```
///
/// # 错误
///
/// - `Error::NotFound`:对象不存在或已被垃圾回收
/// - `Error::Io`:读取失败
/// - `Error::InvalidArgument`ID 格式无效
async fn get(&self, id: &ObjectId) -> crate::Result<Vec<u8>>;
/// 检查对象是否存在
///
/// 快速检查对象是否存在于存储中,不读取实际数据。
///
/// # 参数
///
/// - `id`: 对象 ID
///
/// # 返回
///
/// - `true`: 对象存在
/// - `false`: 对象不存在
///
/// # 性能
///
/// 此方法应该很快(仅检查元数据),不读取实际对象数据。
///
/// # 示例
///
/// ```rust,ignore
/// if repository.exists(&object_id).await {
/// println!("Object exists");
/// }
/// ```
async fn exists(&self, id: &ObjectId) -> bool;
/// 增加引用计数
///
/// 为对象增加一个引用。每次使用对象时都应该增加引用计数。
///
/// # 参数
///
/// - `id`: 对象 ID
///
/// # 返回
///
/// - 成功:返回更新后的引用计数
/// - 失败:如果对象不存在返回 `Error::NotFound`
///
/// # 保证
///
/// - **原子性**:引用计数增加是原子操作
/// - **线程安全**:可以并发调用
///
/// # 示例
///
/// ```rust,ignore
/// let new_count = repository.add_reference(&object_id).await?;
/// println!("Reference count: {}", new_count);
/// ```
///
/// # 使用场景
///
/// - 创建资源索引时引用对象
/// - 生成 Patch 时引用源对象
/// - 任何需要保留对象的场景
async fn add_reference(&self, id: &ObjectId) -> crate::Result<u64>;
/// 减少引用计数
///
/// 为对象减少一个引用。当不再使用对象时应该减少引用计数。
///
/// # 参数
///
/// - `id`: 对象 ID
///
/// # 返回
///
/// - 成功:返回更新后的引用计数
/// - 失败:如果对象不存在返回 `Error::NotFound`
///
/// # 保证
///
/// - **原子性**:引用计数减少是原子操作
/// - **非负性**:引用计数永不为负
///
/// # 注意
///
/// - 引用计数为 0 的对象将被标记为可回收
/// - 不会立即删除,需要显式调用 `gc()`
/// - 减少不存在的引用返回错误
///
/// # 示例
///
/// ```rust,ignore
/// let remaining = repository.remove_reference(&object_id).await?;
/// if remaining == 0 {
/// println!("Object can be garbage collected");
/// }
/// ```
async fn remove_reference(&self, id: &ObjectId) -> crate::Result<u64>;
/// 获取引用计数
///
/// 获取对象的当前引用计数,不修改引用计数。
///
/// # 参数
///
/// - `id`: 对象 ID
///
/// # 返回
///
/// - 成功:返回当前引用计数
/// - 失败:如果对象不存在返回 `Error::NotFound`
///
/// # 示例
///
/// ```rust,ignore
/// let count = repository.get_reference_count(&object_id).await?;
/// println!("Current references: {}", count);
/// ```
async fn get_reference_count(&self, id: &ObjectId) -> crate::Result<u64>;
/// 垃圾回收
///
/// 删除所有引用计数为 0 的对象,释放存储空间。
///
/// # 返回
///
/// - 成功:返回删除的对象数量
/// - 失败:返回错误
///
/// # 注意
///
/// - 此操作可能耗时较长,建议在后台执行
/// - 建议定期执行(如每天一次)
/// - 执行时可能影响性能
///
/// # 示例
///
/// ```rust,ignore
/// let deleted = repository.gc().await?;
/// println!("Deleted {} unused objects", deleted);
/// ```
///
/// # 实现建议
///
/// - 使用标记-清除算法
/// - 考虑增量 GC 避免长时间阻塞
/// - 记录 GC 日志供审计
async fn gc(&self) -> crate::Result<u64>;
/// 从文件存储对象
///
/// 便捷方法:读取文件内容并存储到 CAS。
///
/// # 参数
///
/// - `path`: 文件路径
///
/// # 返回
///
/// - 成功:返回对象 ID
/// - 失败:返回错误(如文件不存在)
///
/// # 示例
///
/// ```rust,ignore
/// let id = repository.store_from_file("asset.bundle").await?;
/// ```
///
/// # 实现
///
/// 默认实现读取整个文件到内存。大文件应考虑流式处理。
async fn store_from_file(&self, path: &Path) -> crate::Result<ObjectId> {
let data = tokio::fs::read(path).await.map_err(|e| {
crate::Error::Io(e)
})?;
self.store(&data).await
}
/// 将对象导出到文件
///
/// 便捷方法:从 CAS 获取对象并写入文件。
///
/// # 参数
///
/// - `id`: 对象 ID
/// - `path`: 目标文件路径
///
/// # 返回
///
/// - 成功:返回 `Ok(())`
/// - 失败:返回错误
///
/// # 示例
///
/// ```rust,ignore
/// repository.export_to_file(&object_id, "output.bin").await?;
/// ```
///
/// # 注意
///
/// - 如果目标文件已存在,将被覆盖
/// - 确保有写入权限
async fn export_to_file(&self, id: &ObjectId, path: &Path) -> crate::Result<()> {
let data = self.get(id).await?;
tokio::fs::write(path, data).await.map_err(|e| {
crate::Error::Io(e)
})?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
/// 测试 ObjectId 类型别名
#[test]
fn test_object_id_is_string() {
let id: ObjectId = "test_id_12345".to_string();
assert_eq!(id, "test_id_12345");
assert!(!id.is_empty());
}
/// 测试 ObjectId 可以作为 HashMap 的 key
#[test]
fn test_object_id_as_hash_key() {
use std::collections::HashMap;
let mut map = HashMap::new();
let id: ObjectId = "hash_abc123".to_string();
map.insert(id.clone(), "value");
assert_eq!(map.get(&id), Some(&"value"));
}
}
+11
View File
@@ -0,0 +1,11 @@
//! 仓储接口模块
//!
//! 定义所有数据访问接口
pub mod cas_repository;
pub mod resource_repository;
pub mod translation_repository;
pub use cas_repository::CasRepository;
pub use resource_repository::ResourceRepository;
pub use translation_repository::TranslationRepository;
@@ -0,0 +1,461 @@
//! 资源仓储接口
//!
//! 管理资源索引和元数据的统一接口。
//!
//! # 设计目标
//!
//! - **快速查询**:支持按 ID、Hash、类型等多种方式查询
//! - **灵活过滤**:提供强大的查询条件组合
//! - **类型安全**:编译时保证类型正确
//! - **异步友好**:所有操作都是异步的
//!
//! # 使用场景
//!
//! - 维护游戏资源索引
//! - 追踪资源下载状态
//! - 管理资源版本
//! - 资源依赖分析
//!
//! # 示例
//!
//! ```rust,ignore
//! use bat_core::repositories::{ResourceRepository, ResourceQuery};
//!
//! // 添加资源
//! let resource_id = repo.add(resource).await?;
//!
//! // 查询资源
//! let resource = repo.find_by_id(&resource_id).await?;
//!
//! // 列出所有 AssetBundle
//! let query = ResourceQuery::by_type(ResourceType::AssetBundle);
//! let bundles = repo.list(query).await?;
//! ```
use crate::domain::{Resource, ResourceType};
use async_trait::async_trait;
/// 资源查询条件
///
/// 用于构建灵活的资源查询。支持按类型、Hash、路径模式过滤。
///
/// # 示例
///
/// ```rust,ignore
/// // 查询所有资源
/// let query = ResourceQuery::all();
///
/// // 按类型查询
/// let query = ResourceQuery::by_type(ResourceType::AssetBundle);
///
/// // 按 Hash 查询
/// let query = ResourceQuery::by_hash("abc123".to_string());
///
/// // 组合查询
/// let query = ResourceQuery {
/// resource_type: Some(ResourceType::AssetBundle),
/// hash: Some("abc123".to_string()),
/// path_pattern: Some("academy-*.bundle".to_string()),
/// };
/// ```
#[derive(Debug, Clone)]
pub struct ResourceQuery {
/// 按资源类型过滤
///
/// 如果为 `Some`,只返回指定类型的资源。
/// 如果为 `None`,返回所有类型。
pub resource_type: Option<ResourceType>,
/// 按 Hash 过滤
///
/// 如果为 `Some`,只返回指定 Hash 的资源。
/// 如果为 `None`,不过滤 Hash。
///
/// Hash 应该是完整的 Hash 字符串(不支持前缀匹配)。
pub hash: Option<String>,
/// 按路径模式过滤
///
/// 如果为 `Some`,只返回路径匹配模式的资源。
/// 如果为 `None`,不过滤路径。
///
/// 支持通配符:
/// - `*` 匹配任意字符(不包括 `/`)
/// - `**` 匹配任意字符(包括 `/`)
/// - `?` 匹配单个字符
///
/// # 示例
///
/// - `"academy-*.bundle"` - 匹配所有以 academy- 开头的 bundle
/// - `"**/*.json"` - 匹配所有 JSON 文件
/// - `"assets/???.png"` - 匹配三个字符的 PNG 文件
pub path_pattern: Option<String>,
}
impl ResourceQuery {
/// 创建空查询(返回所有资源)
///
/// # 返回
///
/// 返回一个不包含任何过滤条件的查询。
///
/// # 示例
///
/// ```rust,ignore
/// let all_resources = repo.list(ResourceQuery::all()).await?;
/// ```
pub fn all() -> Self {
Self {
resource_type: None,
hash: None,
path_pattern: None,
}
}
/// 按类型查询
///
/// # 参数
///
/// - `resource_type`: 资源类型
///
/// # 返回
///
/// 返回只过滤指定类型的查询。
///
/// # 示例
///
/// ```rust,ignore
/// let bundles = repo.list(
/// ResourceQuery::by_type(ResourceType::AssetBundle)
/// ).await?;
/// ```
pub fn by_type(resource_type: ResourceType) -> Self {
Self {
resource_type: Some(resource_type),
hash: None,
path_pattern: None,
}
}
/// 按 Hash 查询
///
/// # 参数
///
/// - `hash`: 资源 Hash
///
/// # 返回
///
/// 返回只过滤指定 Hash 的查询。
///
/// # 示例
///
/// ```rust,ignore
/// let resource = repo.list(
/// ResourceQuery::by_hash("abc123".to_string())
/// ).await?.first();
/// ```
pub fn by_hash(hash: String) -> Self {
Self {
resource_type: None,
hash: Some(hash),
path_pattern: None,
}
}
}
/// 资源仓储接口
///
/// 管理资源索引、元数据和状态的统一接口。
///
/// # 职责
///
/// - **索引管理**:维护资源的索引信息
/// - **快速查询**:支持多种查询方式
/// - **状态追踪**:追踪资源的下载和处理状态
/// - **统计信息**:提供资源统计功能
///
/// # 实现要求
///
/// 实现此接口时应保证:
/// - **线程安全**:所有方法都是 `Send + Sync`
/// - **原子性**:更新操作应该是原子的
/// - **一致性**:索引与实际资源保持一致
/// - **高性能**:查询操作应该很快(建议使用索引)
///
/// # 错误处理
///
/// 所有方法返回 `Result`,可能的错误包括:
/// - `Error::NotFound`:资源不存在
/// - `Error::InvalidArgument`:参数无效
/// - `Error::Other`:数据库或其他错误
#[async_trait]
pub trait ResourceRepository: Send + Sync {
/// 添加资源
///
/// 将资源添加到索引中。如果资源已存在(相同 Hash),则更新。
///
/// # 参数
///
/// - `resource`: 资源对象
///
/// # 返回
///
/// - 成功:返回资源 ID
/// - 失败:返回错误
///
/// # 保证
///
/// - **幂等性**:相同资源多次添加是安全的
/// - **原子性**:添加操作是原子的
///
/// # 示例
///
/// ```rust,ignore
/// let resource = Resource {
/// id: "res_001".to_string(),
/// local_path: PathBuf::from("/path/to/asset.bundle"),
/// entry: ResourceEntry { /* ... */ },
/// };
///
/// let id = repo.add(resource).await?;
/// println!("Added resource with ID: {}", id);
/// ```
///
/// # 实现注意事项
///
/// - 如果 ID 已存在,应该更新而不是报错
/// - 建议对 Hash 和路径建立索引以加快查询
async fn add(&self, resource: Resource) -> crate::Result<String>;
/// 根据 ID 查找资源
///
/// # 参数
///
/// - `id`: 资源 ID
///
/// # 返回
///
/// - 成功:返回资源对象
/// - 失败:如果不存在返回 `Error::NotFound`
///
/// # 性能
///
/// 此方法应该很快(O(1) 或 O(log n)),建议使用主键索引。
///
/// # 示例
///
/// ```rust,ignore
/// let resource = repo.find_by_id("res_001").await?;
/// println!("Found: {:?}", resource);
/// ```
async fn find_by_id(&self, id: &str) -> crate::Result<Resource>;
/// 根据 Hash 查找资源
///
/// # 参数
///
/// - `hash`: 资源 Hash
///
/// # 返回
///
/// - 成功:返回资源对象
/// - 失败:如果不存在返回 `Error::NotFound`
///
/// # 注意
///
/// - 多个资源可能有相同的 Hash(去重)
/// - 此方法返回第一个匹配的资源
/// - 如需查找所有匹配,使用 `list()` 配合 `ResourceQuery::by_hash()`
///
/// # 示例
///
/// ```rust,ignore
/// let resource = repo.find_by_hash("abc123").await?;
/// ```
async fn find_by_hash(&self, hash: &str) -> crate::Result<Resource>;
/// 查询资源列表
///
/// 根据查询条件返回匹配的资源列表。
///
/// # 参数
///
/// - `query`: 查询条件
///
/// # 返回
///
/// - 成功:返回匹配的资源列表(可能为空)
/// - 失败:返回错误
///
/// # 性能
///
/// - 查询应该尽可能使用索引
/// - 对于大结果集,考虑实现分页(未来版本)
///
/// # 示例
///
/// ```rust,ignore
/// // 查询所有 AssetBundle
/// let query = ResourceQuery::by_type(ResourceType::AssetBundle);
/// let bundles = repo.list(query).await?;
/// println!("Found {} bundles", bundles.len());
///
/// // 组合查询
/// let query = ResourceQuery {
/// resource_type: Some(ResourceType::AssetBundle),
/// path_pattern: Some("academy-*.bundle".to_string()),
/// hash: None,
/// };
/// let filtered = repo.list(query).await?;
/// ```
///
/// # 实现建议
///
/// - 对于路径模式匹配,使用 `glob` crate
/// - 考虑结果缓存以提高性能
/// - 对于超大结果集,返回迭代器(未来版本)
async fn list(&self, query: ResourceQuery) -> crate::Result<Vec<Resource>>;
/// 更新资源
///
/// 更新资源的信息。通常用于更新状态、元数据等。
///
/// # 参数
///
/// - `resource`: 更新后的资源对象
///
/// # 返回
///
/// - 成功:返回 `Ok(())`
/// - 失败:如果资源不存在返回 `Error::NotFound`
///
/// # 注意
///
/// - 资源必须已存在(通过 `add()` 添加)
/// - 资源 ID 不能修改
///
/// # 示例
///
/// ```rust,ignore
/// let mut resource = repo.find_by_id("res_001").await?;
/// // 修改资源
/// resource.entry.size = 1024;
/// repo.update(resource).await?;
/// ```
async fn update(&self, resource: Resource) -> crate::Result<()>;
/// 删除资源
///
/// 从索引中删除资源。注意:这不会删除实际的资源文件。
///
/// # 参数
///
/// - `id`: 资源 ID
///
/// # 返回
///
/// - 成功:返回 `Ok(())`
/// - 失败:如果资源不存在返回 `Error::NotFound`
///
/// # 警告
///
/// - 此操作不可逆
/// - 只删除索引,不删除实际文件
/// - 确保没有其他地方引用此资源
///
/// # 示例
///
/// ```rust,ignore
/// repo.delete("res_001").await?;
/// ```
async fn delete(&self, id: &str) -> crate::Result<()>;
/// 统计资源数量
///
/// 根据查询条件统计资源数量,不返回实际资源。
///
/// # 参数
///
/// - `query`: 查询条件
///
/// # 返回
///
/// - 成功:返回资源数量
/// - 失败:返回错误
///
/// # 性能
///
/// 此方法应该比 `list()` 更快,因为不需要返回实际数据。
///
/// # 示例
///
/// ```rust,ignore
/// let total = repo.count(ResourceQuery::all()).await?;
/// let bundles = repo.count(
/// ResourceQuery::by_type(ResourceType::AssetBundle)
/// ).await?;
///
/// println!("Total: {}, Bundles: {}", total, bundles);
/// ```
///
/// # 实现建议
///
/// - 使用 SQL COUNT() 或等效操作
/// - 不要通过 `list().len()` 实现
async fn count(&self, query: ResourceQuery) -> crate::Result<u64>;
}
#[cfg(test)]
mod tests {
use super::*;
/// 测试创建空查询
#[test]
fn test_query_all() {
let query = ResourceQuery::all();
assert!(query.resource_type.is_none());
assert!(query.hash.is_none());
assert!(query.path_pattern.is_none());
}
/// 测试按类型查询
#[test]
fn test_query_by_type() {
let query = ResourceQuery::by_type(ResourceType::AssetBundle);
assert_eq!(query.resource_type, Some(ResourceType::AssetBundle));
assert!(query.hash.is_none());
assert!(query.path_pattern.is_none());
}
/// 测试按 Hash 查询
#[test]
fn test_query_by_hash() {
let query = ResourceQuery::by_hash("abc123".to_string());
assert_eq!(query.hash, Some("abc123".to_string()));
assert!(query.resource_type.is_none());
assert!(query.path_pattern.is_none());
}
/// 测试组合查询
#[test]
fn test_combined_query() {
let query = ResourceQuery {
resource_type: Some(ResourceType::AssetBundle),
hash: Some("hash123".to_string()),
path_pattern: Some("*.bundle".to_string()),
};
assert_eq!(query.resource_type, Some(ResourceType::AssetBundle));
assert_eq!(query.hash, Some("hash123".to_string()));
assert_eq!(query.path_pattern, Some("*.bundle".to_string()));
}
/// 测试 ResourceQuery 可以被克隆
#[test]
fn test_query_clone() {
let query1 = ResourceQuery::by_type(ResourceType::Manifest);
let query2 = query1.clone();
assert_eq!(query2.resource_type, Some(ResourceType::Manifest));
}
}
@@ -0,0 +1,522 @@
//! 翻译仓储接口
//!
//! 管理翻译记忆库(Translation Memory)的统一接口。
//!
//! # 什么是翻译记忆库?
//!
//! 翻译记忆库是一个数据库,存储源文本和对应的翻译。当遇到相同或相似的文本时,
//! 可以直接使用或参考之前的翻译,从而:
//! - 提高翻译一致性
//! - 减少翻译成本
//! - 加快翻译速度
//!
//! # 核心功能
//!
//! - **精确匹配**:完全相同的文本直接复用翻译
//! - **模糊匹配**:相似文本提供参考翻译
//! - **批量操作**:支持批量保存提高效率
//! - **状态管理**:追踪翻译的审核状态
//!
//! # 使用场景
//!
//! - 游戏文本翻译
//! - UI 文本翻译
//! - 剧情对话翻译
//! - 角色名称翻译
//!
//! # 示例
//!
//! ```rust,ignore
//! use bat_core::repositories::TranslationRepository;
//!
//! // 保存翻译
//! let translation = TranslatedText {
//! source_id: "text_001".to_string(),
//! translation: "你好".to_string(),
//! provider: "AI".to_string(),
//! confidence: 0.95,
//! status: TranslationStatus::Translated,
//! };
//! repo.save(translation).await?;
//!
//! // 精确查找
//! let trans = repo.find_exact("text_001").await?;
//!
//! // 模糊查找
//! let matches = repo.find_fuzzy("Hello", 0.8, 5).await?;
//! for m in matches {
//! println!("相似度: {}, 翻译: {}", m.similarity, m.translation.translation);
//! }
//! ```
use crate::domain::{SourceText, TranslatedText, TranslationStatus};
use async_trait::async_trait;
/// 模糊匹配结果
///
/// 当进行模糊匹配时,返回源文本、翻译和相似度。
///
/// # 字段说明
///
/// - `source`: 源文本对象
/// - `translation`: 对应的翻译
/// - `similarity`: 相似度(0.0 - 1.01.0 表示完全相同)
///
/// # 相似度计算
///
/// 相似度通常使用以下算法之一:
/// - Levenshtein 距离(编辑距离)
/// - Jaro-Winkler 相似度
/// - 余弦相似度(基于词向量)
///
/// # 示例
///
/// ```rust,ignore
/// for match_result in fuzzy_matches {
/// if match_result.similarity > 0.9 {
/// println!("高度相似: {}", match_result.translation.translation);
/// }
/// }
/// ```
#[derive(Debug, Clone)]
pub struct FuzzyMatch {
/// 源文本
///
/// 匹配到的源文本对象,包含原始内容和 ID。
pub source: SourceText,
/// 翻译文本
///
/// 源文本对应的翻译,包含翻译内容、提供商、置信度等信息。
pub translation: TranslatedText,
/// 相似度(0.0 - 1.0
///
/// 表示查询文本与此源文本的相似程度:
/// - 1.0:完全相同
/// - 0.9-0.99:高度相似,可能只是标点或格式差异
/// - 0.8-0.89:较为相似,可以作为参考
/// - < 0.8:相似度较低,参考价值有限
pub similarity: f32,
}
/// 翻译仓储接口
///
/// 管理翻译记忆库,支持精确匹配和模糊匹配。
///
/// # 设计原则
///
/// - **高性能**:查询操作应该很快(使用索引和缓存)
/// - **可扩展**:支持百万级翻译条目
/// - **准确性**:模糊匹配算法应该准确
/// - **一致性**:保证数据一致性
///
/// # 实现要求
///
/// 实现此接口时应保证:
/// - **线程安全**:所有方法都是 `Send + Sync`
/// - **原子性**:更新操作应该是原子的
/// - **唯一性**:同一 source_id 只有一条记录
/// - **索引优化**:为常用查询建立索引
///
/// # 性能建议
///
/// - 对 source_id 建立主键索引
/// - 对源文本内容建立全文索引
/// - 使用缓存加速热点查询
/// - 模糊匹配考虑使用向量数据库(如 Milvus)
///
/// # 错误处理
///
/// 所有方法返回 `Result`,可能的错误包括:
/// - `Error::NotFound`:翻译不存在
/// - `Error::InvalidArgument`:参数无效(如相似度 > 1.0)
/// - `Error::Other`:数据库或其他错误
#[async_trait]
pub trait TranslationRepository: Send + Sync {
/// 保存翻译
///
/// 保存或更新翻译到记忆库。如果 source_id 已存在,则更新。
///
/// # 参数
///
/// - `translation`: 翻译对象
///
/// # 返回
///
/// - 成功:返回 `Ok(())`
/// - 失败:返回错误
///
/// # 行为
///
/// - 如果 source_id 不存在,创建新记录
/// - 如果 source_id 已存在,更新记录
/// - 幂等操作:多次保存相同翻译是安全的
///
/// # 示例
///
/// ```rust,ignore
/// let translation = TranslatedText {
/// source_id: "greeting_001".to_string(),
/// translation: "你好,世界!".to_string(),
/// provider: "DeepL".to_string(),
/// confidence: 0.98,
/// status: TranslationStatus::Translated,
/// };
///
/// repo.save(translation).await?;
/// ```
///
/// # 实现注意事项
///
/// - 使用 UPSERT 或等效操作
/// - 更新时保留创建时间,更新修改时间
/// - 考虑记录翻译历史(可选)
async fn save(&self, translation: TranslatedText) -> crate::Result<()>;
/// 精确匹配查找翻译
///
/// 根据源文本 ID 精确查找对应的翻译。
///
/// # 参数
///
/// - `source_id`: 源文本 ID
///
/// # 返回
///
/// - 成功:返回翻译对象
/// - 失败:如果不存在返回 `Error::NotFound`
///
/// # 性能
///
/// 此方法应该非常快(O(1) 或 O(log n)),使用主键索引。
///
/// # 示例
///
/// ```rust,ignore
/// match repo.find_exact("greeting_001").await {
/// Ok(trans) => println!("翻译: {}", trans.translation),
/// Err(_) => println!("未找到翻译"),
/// }
/// ```
async fn find_exact(&self, source_id: &str) -> crate::Result<TranslatedText>;
/// 模糊匹配查找翻译
///
/// 查找与给定文本相似的已翻译文本,用于提供翻译参考。
///
/// # 参数
///
/// - `content`: 源文本内容(不是 ID
/// - `threshold`: 相似度阈值(0.0 - 1.0),只返回相似度 >= threshold 的结果
/// - `limit`: 最多返回结果数(建议 5-20)
///
/// # 返回
///
/// - 成功:返回匹配结果列表(按相似度降序排列,可能为空)
/// - 失败:返回错误
///
/// # 参数约束
///
/// - `threshold` 应在 [0.0, 1.0] 范围内,推荐 0.7-0.9
/// - `limit` 应 > 0,推荐 5-20(太大影响性能)
///
/// # 性能考虑
///
/// 模糊匹配是昂贵的操作,实现时应考虑:
/// - 使用全文索引加速
/// - 缓存热点查询
/// - 限制查询范围(如只在最近 N 条中查找)
/// - 考虑使用向量数据库
///
/// # 示例
///
/// ```rust,ignore
/// // 查找相似翻译
/// let matches = repo.find_fuzzy("Hello, world!", 0.8, 5).await?;
///
/// for m in matches {
/// println!("原文: {}", m.source.content);
/// println!("译文: {}", m.translation.translation);
/// println!("相似度: {:.2}%", m.similarity * 100.0);
/// println!("---");
/// }
/// ```
///
/// # 实现建议
///
/// 简单实现(适合小规模):
/// ```rust,ignore
/// // 使用 Levenshtein 距离
/// use strsim::jaro_winkler;
///
/// for text in all_texts {
/// let sim = jaro_winkler(query, &text.content);
/// if sim >= threshold {
/// results.push(FuzzyMatch { similarity: sim, ... });
/// }
/// }
/// results.sort_by(|a, b| b.similarity.partial_cmp(&a.similarity).unwrap());
/// ```
///
/// 高级实现(适合大规模):
/// - 使用 PostgreSQL pg_trgm 扩展
/// - 使用 Elasticsearch 全文搜索
/// - 使用向量数据库(Milvus, Qdrant
async fn find_fuzzy(
&self,
content: &str,
threshold: f32,
limit: usize,
) -> crate::Result<Vec<FuzzyMatch>>;
/// 更新翻译状态
///
/// 更新翻译的审核状态,不修改翻译内容。
///
/// # 参数
///
/// - `source_id`: 源文本 ID
/// - `status`: 新状态
///
/// # 返回
///
/// - 成功:返回 `Ok(())`
/// - 失败:如果翻译不存在返回 `Error::NotFound`
///
/// # 使用场景
///
/// - 标记翻译为"待审核"
/// - 审核通过后标记为"已审核"
/// - 发现问题时标记为"待翻译"
///
/// # 示例
///
/// ```rust,ignore
/// // 审核通过
/// repo.update_status("greeting_001", TranslationStatus::Reviewed).await?;
/// ```
///
/// # 实现建议
///
/// - 使用 UPDATE 语句,只更新 status 字段
/// - 记录状态变更时间
/// - 可选:记录状态变更历史
async fn update_status(
&self,
source_id: &str,
status: TranslationStatus,
) -> crate::Result<()>;
/// 批量保存翻译
///
/// 一次性保存多个翻译,比多次调用 `save()` 更高效。
///
/// # 参数
///
/// - `translations`: 翻译列表
///
/// # 返回
///
/// - 成功:返回实际保存的数量
/// - 失败:返回错误(应该是全部成功或全部失败)
///
/// # 行为
///
/// - 所有翻译在一个事务中保存
/// - 如果任何一个失败,全部回滚
/// - 原子操作:要么全部成功,要么全部失败
///
/// # 性能
///
/// 批量保存比循环调用 `save()` 快得多(可能快 10-100 倍)。
///
/// # 示例
///
/// ```rust,ignore
/// let translations = vec![
/// TranslatedText { source_id: "001".to_string(), ... },
/// TranslatedText { source_id: "002".to_string(), ... },
/// TranslatedText { source_id: "003".to_string(), ... },
/// ];
///
/// let count = repo.save_batch(translations).await?;
/// println!("保存了 {} 条翻译", count);
/// ```
///
/// # 实现建议
///
/// - 使用批量 INSERT/UPSERT
/// - 使用事务保证原子性
/// - 对于超大批次,考虑分批处理
async fn save_batch(&self, translations: Vec<TranslatedText>) -> crate::Result<usize>;
/// 统计翻译数量
///
/// 统计翻译记忆库中的翻译数量,可以按状态过滤。
///
/// # 参数
///
/// - `status`: 可选的状态过滤
/// - `Some(status)`: 只统计指定状态的翻译
/// - `None`: 统计所有翻译
///
/// # 返回
///
/// - 成功:返回翻译数量
/// - 失败:返回错误
///
/// # 示例
///
/// ```rust,ignore
/// // 统计所有翻译
/// let total = repo.count(None).await?;
///
/// // 统计已审核的翻译
/// let reviewed = repo.count(Some(TranslationStatus::Reviewed)).await?;
///
/// // 统计待审核的翻译
/// let pending = repo.count(Some(TranslationStatus::Translated)).await?;
///
/// println!("总计: {}, 已审核: {}, 待审核: {}", total, reviewed, pending);
/// ```
///
/// # 实现建议
///
/// - 使用 SQL COUNT() 或等效操作
/// - 不要通过查询所有记录再计数
/// - 考虑缓存结果(定期更新)
async fn count(&self, status: Option<TranslationStatus>) -> crate::Result<u64>;
/// 删除翻译
///
/// 从翻译记忆库中删除指定的翻译。
///
/// # 参数
///
/// - `source_id`: 源文本 ID
///
/// # 返回
///
/// - 成功:返回 `Ok(())`
/// - 失败:如果翻译不存在返回 `Error::NotFound`
///
/// # 警告
///
/// - 此操作不可逆
/// - 删除前确认不再需要此翻译
/// - 考虑使用软删除(标记为已删除)而不是物理删除
///
/// # 示例
///
/// ```rust,ignore
/// // 删除错误的翻译
/// repo.delete("bad_translation_001").await?;
/// ```
///
/// # 实现建议
///
/// - 考虑实现软删除(添加 deleted_at 字段)
/// - 记录删除日志供审计
/// - 提供恢复机制(可选)
async fn delete(&self, source_id: &str) -> crate::Result<()>;
}
#[cfg(test)]
mod tests {
use super::*;
/// 测试创建模糊匹配结果
#[test]
fn test_fuzzy_match_creation() {
let source = SourceText {
id: "test_001".to_string(),
content: "Hello".to_string(),
};
let translation = TranslatedText {
source_id: "test_001".to_string(),
translation: "你好".to_string(),
provider: "test".to_string(),
confidence: 0.95,
status: TranslationStatus::Translated,
};
let match_result = FuzzyMatch {
source: source.clone(),
translation: translation.clone(),
similarity: 0.9,
};
assert_eq!(match_result.similarity, 0.9);
assert_eq!(match_result.source.id, "test_001");
assert_eq!(match_result.translation.translation, "你好");
}
/// 测试模糊匹配相似度范围
#[test]
fn test_fuzzy_match_similarity_range() {
let source = SourceText {
id: "test".to_string(),
content: "test".to_string(),
};
let translation = TranslatedText {
source_id: "test".to_string(),
translation: "测试".to_string(),
provider: "test".to_string(),
confidence: 1.0,
status: TranslationStatus::Translated,
};
// 完全匹配
let exact = FuzzyMatch {
source: source.clone(),
translation: translation.clone(),
similarity: 1.0,
};
assert!((exact.similarity - 1.0).abs() < f32::EPSILON);
// 高度相似
let high = FuzzyMatch {
source: source.clone(),
translation: translation.clone(),
similarity: 0.95,
};
assert!(high.similarity >= 0.9 && high.similarity < 1.0);
// 中等相似
let medium = FuzzyMatch {
source: source.clone(),
translation: translation.clone(),
similarity: 0.75,
};
assert!(medium.similarity >= 0.7 && medium.similarity < 0.9);
}
/// 测试 FuzzyMatch 可以被克隆
#[test]
fn test_fuzzy_match_clone() {
let source = SourceText {
id: "test".to_string(),
content: "test".to_string(),
};
let translation = TranslatedText {
source_id: "test".to_string(),
translation: "测试".to_string(),
provider: "test".to_string(),
confidence: 1.0,
status: TranslationStatus::Translated,
};
let match1 = FuzzyMatch {
source,
translation,
similarity: 0.9,
};
let match2 = match1.clone();
assert_eq!(match2.similarity, 0.9);
}
}
+3
View File
@@ -0,0 +1,3 @@
//! 领域服务模块(占位)
// 将在后续实现