You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

364 lines
12 KiB
C#

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

// ============================================================================
// 【文件说明】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;
/// <summary>
/// 网页文档服务类。
/// <para>
/// 【服务职责】
/// 1. 管理网页文档的增删改查
/// 2. 提供文档密钥验证功能
/// 3. 文档变更时自动重建搜索索引
/// </para>
/// <para>
/// 【软删除策略】
/// 使用 IsDelete 字段标记删除状态("0"正常,"1"已删除),
/// 而不是物理删除,便于数据恢复和审计。
/// </para>
/// </summary>
public class HwWebDocumentService : ITransient
{
/// <summary>
/// MyBatis 映射器名称(保留用于回滚)。
/// </summary>
private const string Mapper = "HwWebDocumentMapper";
/// <summary>
/// MyBatis 执行器(保留用于回滚)。
/// </summary>
private readonly HwPortalMyBatisExecutor _executor;
/// <summary>
/// SqlSugar 数据访问对象。
/// </summary>
private readonly ISqlSugarClient _db;
/// <summary>
/// 搜索索引重建服务。
/// </summary>
private readonly IHwSearchRebuildService _searchRebuildService;
/// <summary>
/// 日志记录器。
/// </summary>
private readonly ILogger<HwWebDocumentService> _logger;
/// <summary>
/// 构造函数(依赖注入)。
/// </summary>
/// <param name="executor">MyBatis 执行器</param>
/// <param name="db">SqlSugar 数据访问对象</param>
/// <param name="searchRebuildService">搜索索引重建服务</param>
/// <param name="logger">日志记录器</param>
public HwWebDocumentService(HwPortalMyBatisExecutor executor, ISqlSugarClient db, IHwSearchRebuildService searchRebuildService, ILogger<HwWebDocumentService> logger)
{
_executor = executor;
_db = db;
_searchRebuildService = searchRebuildService;
_logger = logger;
}
/// <summary>
/// 根据文档ID查询文档信息。
/// <para>
/// 【软删除过滤】
/// 查询时自动过滤已删除的记录IsDelete == "0")。
/// </para>
/// </summary>
/// <param name="documentId">文档ID</param>
/// <returns>文档实体</returns>
public async Task<HwWebDocument> SelectHwWebDocumentByDocumentId(string documentId)
{
// 回滚到 XML 方案时可直接恢复:
// return await _executor.QuerySingleAsync<HwWebDocument>(Mapper, "selectHwWebDocumentByDocumentId", new { documentId });
return await _db.Queryable<HwWebDocument>()
.Where(item => item.IsDelete == "0" && item.DocumentId == documentId)
.FirstAsync();
}
/// <summary>
/// 查询文档列表。
/// <para>
/// 【动态查询条件】
/// 支持按文档ID、租户ID、文档地址、网站代码、密钥、JSON、类型等条件筛选。
/// 所有条件都是可选的,有值时才添加到 WHERE 子句。
/// </para>
/// </summary>
/// <param name="input">查询条件</param>
/// <returns>文档列表</returns>
public async Task<List<HwWebDocument>> SelectHwWebDocumentList(HwWebDocument input)
{
HwWebDocument query = input ?? new HwWebDocument();
// 回滚到 XML 方案时可直接恢复:
// return await _executor.QueryListAsync<HwWebDocument>(Mapper, "selectHwWebDocumentList", query);
return await _db.Queryable<HwWebDocument>()
.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();
}
/// <summary>
/// 新增文档信息。
/// <para>
/// 【搜索索引重建】
/// 文档新增后,自动触发搜索索引重建,确保新文档可被搜索到。
/// </para>
/// </summary>
/// <param name="input">文档数据</param>
/// <returns>影响行数</returns>
public async Task<int> 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;
}
/// <summary>
/// 更新文档信息。
/// <para>
/// 【密钥字段特殊处理】
/// Why这里必须保留"传空串等于清空口令"的兼容语义,否则旧前端无法撤销文档密钥。
///
/// 如果传入的 SecretKey 为 null自动设为空字符串。
/// 如果传入的 SecretKey 为空字符串,在更新时转为 null。
/// </para>
/// </summary>
/// <param name="input">更新的数据</param>
/// <returns>影响行数</returns>
public async Task<int> 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;
}
/// <summary>
/// 批量删除文档信息(软删除)。
/// <para>
/// 【软删除实现】
/// 不是物理删除,而是将 IsDelete 字段更新为"1"。
/// 这样可以保留数据用于审计,也支持恢复操作。
/// </para>
/// </summary>
/// <param name="documentIds">文档ID数组</param>
/// <returns>影响行数</returns>
public async Task<int> DeleteHwWebDocumentByDocumentIds(string[] documentIds)
{
// 回滚到 XML 方案时可直接恢复:
// int rows = await _executor.ExecuteAsync(Mapper, "deleteHwWebDocumentByDocumentIds", new { array = documentIds });
// 【查询待删除的文档】
List<HwWebDocument> documents = await _db.Queryable<HwWebDocument>()
.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;
}
/// <summary>
/// 验证文档密钥并获取文档地址。
/// <para>
/// 【密钥验证逻辑】
/// 1. 如果文档没有设置密钥SecretKey 为空),直接返回地址
/// 2. 如果提供了正确的密钥,返回地址
/// 3. 如果密钥错误或为空,抛出异常
/// </para>
/// <para>
/// 【Oops.Oh 异常处理】
/// Furion 框架提供的友好异常抛出方式,
/// 会自动包装为统一的 API 响应格式。
/// </para>
/// </summary>
/// <param name="documentId">文档ID</param>
/// <param name="providedKey">提供的密钥</param>
/// <returns>文档地址</returns>
public async Task<string> 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("密钥错误");
}
/// <summary>
/// 静默重建搜索索引。
/// <para>
/// 【异步重建】
/// 文档变更后,异步触发搜索索引重建。
/// 使用 try-catch 包裹,确保索引重建失败不影响主业务流程。
/// </para>
/// <para>
/// 【日志记录】
/// 如果重建失败,记录错误日志,便于排查问题。
/// </para>
/// </summary>
private async Task RebuildSearchIndexQuietly()
{
try
{
await _searchRebuildService.RebuildAllAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "rebuild portal search index failed after hw_web_document changed");
}
}
}