// ============================================================================ // 【文件说明】HwWebDocumentService.cs - 网页文档服务类 // ============================================================================ // 这个服务类负责处理网页文档的业务逻辑,包括: // - 文档信息的 CRUD 操作 // - 文档密钥验证 // - 搜索索引重建 // // 【业务背景】 // 网页文档模块用于管理网站的文档资源,支持密钥保护功能。 // 当文档被修改时,会自动触发搜索索引重建。 // // 【与 Java Spring Boot 的对比】 // Java Spring Boot: // @Service // public class HwWebDocumentServiceImpl implements HwWebDocumentService { ... } // // ASP.NET Core + Furion: // public class HwWebDocumentService : ITransient { ... } // ============================================================================ namespace Admin.NET.Plugin.HwPortal; /// /// 网页文档服务类。 /// /// 【服务职责】 /// 1. 管理网页文档的增删改查 /// 2. 提供文档密钥验证功能 /// 3. 文档变更时自动重建搜索索引 /// /// /// 【软删除策略】 /// 使用 IsDelete 字段标记删除状态("0"正常,"1"已删除), /// 而不是物理删除,便于数据恢复和审计。 /// /// public class HwWebDocumentService : ITransient { /// /// MyBatis 映射器名称(保留用于回滚)。 /// private const string Mapper = "HwWebDocumentMapper"; /// /// MyBatis 执行器(保留用于回滚)。 /// private readonly HwPortalMyBatisExecutor _executor; /// /// SqlSugar 数据访问对象。 /// private readonly ISqlSugarClient _db; /// /// 搜索索引重建服务。 /// private readonly IHwSearchRebuildService _searchRebuildService; /// /// 日志记录器。 /// private readonly ILogger _logger; /// /// 构造函数(依赖注入)。 /// /// MyBatis 执行器 /// SqlSugar 数据访问对象 /// 搜索索引重建服务 /// 日志记录器 public HwWebDocumentService(HwPortalMyBatisExecutor executor, ISqlSugarClient db, IHwSearchRebuildService searchRebuildService, ILogger logger) { _executor = executor; _db = db; _searchRebuildService = searchRebuildService; _logger = logger; } /// /// 根据文档ID查询文档信息。 /// /// 【软删除过滤】 /// 查询时自动过滤已删除的记录(IsDelete == "0")。 /// /// /// 文档ID /// 文档实体 public async Task SelectHwWebDocumentByDocumentId(string documentId) { // 回滚到 XML 方案时可直接恢复: // return await _executor.QuerySingleAsync(Mapper, "selectHwWebDocumentByDocumentId", new { documentId }); return await _db.Queryable() .Where(item => item.IsDelete == "0" && item.DocumentId == documentId) .FirstAsync(); } /// /// 查询文档列表。 /// /// 【动态查询条件】 /// 支持按文档ID、租户ID、文档地址、网站代码、密钥、JSON、类型等条件筛选。 /// 所有条件都是可选的,有值时才添加到 WHERE 子句。 /// /// /// 查询条件 /// 文档列表 public async Task> SelectHwWebDocumentList(HwWebDocument input) { HwWebDocument query = input ?? new HwWebDocument(); // 回滚到 XML 方案时可直接恢复: // return await _executor.QueryListAsync(Mapper, "selectHwWebDocumentList", query); return await _db.Queryable() .Where(item => item.IsDelete == "0") .WhereIF(!string.IsNullOrWhiteSpace(query.DocumentId), item => item.DocumentId == query.DocumentId) .WhereIF(query.TenantId.HasValue, item => item.TenantId == query.TenantId) .WhereIF(!string.IsNullOrWhiteSpace(query.DocumentAddress), item => item.DocumentAddress == query.DocumentAddress) .WhereIF(!string.IsNullOrWhiteSpace(query.WebCode), item => item.WebCode == query.WebCode) .WhereIF(!string.IsNullOrWhiteSpace(query.SecretKey), item => item.SecretKey == query.SecretKey) .WhereIF(query.Json != null && query.Json != string.Empty, item => item.Json == query.Json) .WhereIF(query.Type != null && query.Type != string.Empty, item => item.Type == query.Type) .ToListAsync(); } /// /// 新增文档信息。 /// /// 【搜索索引重建】 /// 文档新增后,自动触发搜索索引重建,确保新文档可被搜索到。 /// /// /// 文档数据 /// 影响行数 public async Task InsertHwWebDocument(HwWebDocument input) { // 【审计字段】 input.CreateTime = HwPortalContextHelper.Now(); // 【软删除标记初始化】 // 如果未指定删除标记,默认为"0"(未删除) input.IsDelete = string.IsNullOrWhiteSpace(input.IsDelete) ? "0" : input.IsDelete; // 回滚到 XML 方案时可直接恢复: // int rows = await _executor.ExecuteAsync(Mapper, "insertHwWebDocument", input); int rows = await _db.Insertable(input).ExecuteCommandAsync(); // 【搜索索引重建】 // 文档变更后,异步重建搜索索引 if (rows > 0) { await RebuildSearchIndexQuietly(); } return rows; } /// /// 更新文档信息。 /// /// 【密钥字段特殊处理】 /// Why:这里必须保留"传空串等于清空口令"的兼容语义,否则旧前端无法撤销文档密钥。 /// /// 如果传入的 SecretKey 为 null,自动设为空字符串。 /// 如果传入的 SecretKey 为空字符串,在更新时转为 null。 /// /// /// 更新的数据 /// 影响行数 public async Task UpdateHwWebDocument(HwWebDocument input) { // 【密钥字段默认值】 // 如果密钥为 null,设为空字符串,避免空引用异常 if (input.SecretKey == null) { input.SecretKey = string.Empty; } // 回滚到 XML 方案时可直接恢复: // int rows = await _executor.ExecuteAsync(Mapper, "updateHwWebDocument", input); HwWebDocument current = await SelectHwWebDocumentByDocumentId(input.DocumentId); if (current == null) { return 0; } // 【文档ID】 if (input.DocumentId != null) { current.DocumentId = input.DocumentId; } // 【租户ID】 if (input.TenantId.HasValue) { current.TenantId = input.TenantId; } // 【文档地址】 if (input.DocumentAddress != null) { current.DocumentAddress = input.DocumentAddress; } // 【审计字段】 if (input.CreateTime.HasValue) { current.CreateTime = input.CreateTime; } // 【网站代码】 if (input.WebCode != null) { current.WebCode = input.WebCode; } // 【密钥字段特殊处理】 // Why:这里必须保留"传空串等于清空口令"的兼容语义,否则旧前端无法撤销文档密钥。 // 如果传入空字符串,转为 null 存储 if (input.SecretKey != null) { current.SecretKey = string.IsNullOrEmpty(input.SecretKey) ? null : input.SecretKey; } // 【JSON数据】 if (input.Json != null) { current.Json = input.Json; } // 【类型】 if (input.Type != null) { current.Type = input.Type; } int rows = await _db.Updateable(current).ExecuteCommandAsync(); // 【搜索索引重建】 if (rows > 0) { await RebuildSearchIndexQuietly(); } return rows; } /// /// 批量删除文档信息(软删除)。 /// /// 【软删除实现】 /// 不是物理删除,而是将 IsDelete 字段更新为"1"。 /// 这样可以保留数据用于审计,也支持恢复操作。 /// /// /// 文档ID数组 /// 影响行数 public async Task DeleteHwWebDocumentByDocumentIds(string[] documentIds) { // 回滚到 XML 方案时可直接恢复: // int rows = await _executor.ExecuteAsync(Mapper, "deleteHwWebDocumentByDocumentIds", new { array = documentIds }); // 【查询待删除的文档】 List documents = await _db.Queryable() .Where(item => documentIds.Contains(item.DocumentId)) .ToListAsync(); // 【软删除标记】 foreach (HwWebDocument document in documents) { document.IsDelete = "1"; } // 【批量更新】 // 只更新 IsDelete 字段,使用 UpdateColumns 指定更新字段 int rows = documents.Count == 0 ? 0 : await _db.Updateable(documents) .UpdateColumns(item => new { item.IsDelete }) .ExecuteCommandAsync(); // 【搜索索引重建】 if (rows > 0) { await RebuildSearchIndexQuietly(); } return rows; } /// /// 验证文档密钥并获取文档地址。 /// /// 【密钥验证逻辑】 /// 1. 如果文档没有设置密钥(SecretKey 为空),直接返回地址 /// 2. 如果提供了正确的密钥,返回地址 /// 3. 如果密钥错误或为空,抛出异常 /// /// /// 【Oops.Oh 异常处理】 /// Furion 框架提供的友好异常抛出方式, /// 会自动包装为统一的 API 响应格式。 /// /// /// 文档ID /// 提供的密钥 /// 文档地址 public async Task VerifyAndGetDocumentAddress(string documentId, string providedKey) { // 【查询文档】 HwWebDocument document = await SelectHwWebDocumentByDocumentId(documentId); if (document == null) { throw Oops.Oh("文件不存在"); } // 【无密钥保护】 // 如果文档没有设置密钥,直接返回地址 if (string.IsNullOrWhiteSpace(document.SecretKey)) { return document.DocumentAddress; } // 【密钥验证】 // 去除前后空格后比较 string trimmedProvided = providedKey?.Trim(); if (string.IsNullOrWhiteSpace(trimmedProvided)) { throw Oops.Oh("密钥不能为空"); } // 【密钥匹配】 // StringComparison.Ordinal 表示区分大小写的二进制比较 if (string.Equals(document.SecretKey.Trim(), trimmedProvided, StringComparison.Ordinal)) { return document.DocumentAddress; } // 【密钥错误】 throw Oops.Oh("密钥错误"); } /// /// 静默重建搜索索引。 /// /// 【异步重建】 /// 文档变更后,异步触发搜索索引重建。 /// 使用 try-catch 包裹,确保索引重建失败不影响主业务流程。 /// /// /// 【日志记录】 /// 如果重建失败,记录错误日志,便于排查问题。 /// /// private async Task RebuildSearchIndexQuietly() { try { await _searchRebuildService.RebuildAllAsync(); } catch (Exception ex) { _logger.LogError(ex, "rebuild portal search index failed after hw_web_document changed"); } } }