|
|
// ============================================================================
|
|
|
// 【文件说明】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");
|
|
|
}
|
|
|
}
|
|
|
}
|