mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-07-22 06:35:16 +08:00
chore: establish development baseline
This commit is contained in:
@@ -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(_)));
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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>;
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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.0,1.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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
//! 领域服务模块(占位)
|
||||
|
||||
// 将在后续实现
|
||||
Reference in New Issue
Block a user