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
+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);
}
}