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.

398 lines
15 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.

// ============================================================================
// 【文件说明】HwWebService.cs - 官网页面业务服务类
// ============================================================================
// 这个服务类负责处理官网页面的业务逻辑。
//
// 【分层架构说明】
// Controller控制器层-> Service服务层-> Repository数据访问层
//
// 控制器只负责:接收请求、调用服务、返回响应
// 服务层负责:业务逻辑、数据组装、事务控制
// 数据访问层负责:数据库 CRUD 操作
//
// 【与 Java Spring Boot 的对比】
// Java Spring Boot:
// @Service
// public class HwWebServiceImpl implements HwWebService { ... }
//
// ASP.NET Core + Furion:
// public class HwWebService : ITransient { ... }
//
// ITransient 是 Furion 的"生命周期接口",表示"瞬态服务"
// - 每次请求都创建新实例
// - 类似 Java Spring 的 @Scope("prototype")
//
// Furion 支持三种生命周期:
// - ITransient瞬态每次请求新实例
// - IScoped作用域同一请求内共享实例
// - ISingleton单例全局共享一个实例
// ============================================================================
namespace Admin.NET.Plugin.HwPortal;
/// <summary>
/// 官网页面业务服务类。
/// <para>
/// 【C# 语法知识点 - 接口实现】
/// public class HwWebService : ITransient
///
/// ITransient 是 Furion 框架的"标记接口"Marker Interface
/// 它没有任何方法,只是用来标记服务的生命周期。
///
/// 对比 Java Spring
/// Java Spring 用 @Service + @Scope 注解来定义服务:
/// @Service
/// @Scope("prototype") // 等价于 ITransient
/// public class HwWebService { ... }
///
/// C# Furion 用接口来标记,更符合"接口隔离原则"。
/// </para>
/// <para>
/// 【MyBatis 风格的数据访问】
/// 这个服务使用 HwPortalMyBatisExecutor 执行 SQL。
// 这是一种"类 MyBatis"的设计:
/// - SQL 写在 XML 文件中Sql/HwWebMapper.xml
/// - 通过执行器调用 XML 中定义的 SQL
/// - 参数通过匿名对象传递
///
/// 对比 Java MyBatis
/// Java MyBatis:
/// @Mapper
/// public interface HwWebMapper {
/// HwWeb selectHwWebByWebcode(@Param("webCode") Long webCode);
/// }
///
/// C# 这里用执行器模式,更灵活但需要手动调用。
/// </para>
/// </summary>
public class HwWebService : ITransient
{
/// <summary>
/// MyBatis 映射器名称。
/// <para>
/// 【C# 语法知识点 - const 常量】
/// const 是"编译期常量",值在编译时就确定了。
///
/// 对比 Java
/// Java: private static final String MAPPER = "HwWebMapper";
/// C#: private const string Mapper = "HwWebMapper";
///
/// C# 的命名约定:
/// - const 通常用 PascalCase首字母大写
/// - Java 的 static final 通常用 UPPER_SNAKE_CASE
/// </para>
/// </summary>
private const string Mapper = "HwWebMapper";
/// <summary>
/// MyBatis 执行器。
/// <para>
/// 【依赖注入】
/// 通过构造函数注入,和控制器注入服务的方式一样。
///
/// HwPortalMyBatisExecutor 是自定义的执行器,封装了 MyBatis 风格的 SQL 执行逻辑。
/// </para>
/// </summary>
private readonly HwPortalMyBatisExecutor _executor;
/// <summary>
/// SqlSugar 数据访问对象。
/// </summary>
private readonly ISqlSugarClient _db;
/// <summary>
/// 搜索索引重建服务。
/// </summary>
private readonly IHwSearchRebuildService _searchRebuildService;
/// <summary>
/// 日志记录器。
/// <para>
/// 【C# 语法知识点 - 泛型日志接口】
/// ILogger&lt;HwWebService&gt; 是 Microsoft.Extensions.Logging 提供的日志接口。
/// 泛型参数 T 用于标识日志的类别,通常传入当前类。
///
/// 使用方式:
/// _logger.LogInformation("信息日志");
/// _logger.LogWarning("警告日志");
/// _logger.LogError("错误日志");
/// _logger.LogError(ex, "错误日志,带异常信息");
///
/// 对比 Java SLF4J
/// Java: private static final Logger log = LoggerFactory.getLogger(HwWebService.class);
/// C#: private readonly ILogger&lt;HwWebService&gt; _logger;
///
/// 两者的用法类似,但 C# 的 ILogger 是接口,通过 DI 注入。
/// </para>
/// </summary>
private readonly ILogger<HwWebService> _logger;
/// <summary>
/// 构造函数(依赖注入)。
/// </summary>
/// <param name="executor">MyBatis 执行器</param>
/// <param name="searchRebuildService">搜索索引重建服务</param>
/// <param name="logger">日志记录器</param>
public HwWebService(HwPortalMyBatisExecutor executor, ISqlSugarClient db, IHwSearchRebuildService searchRebuildService, ILogger<HwWebService> logger)
{
_executor = executor;
_db = db;
_searchRebuildService = searchRebuildService;
_logger = logger;
}
/// <summary>
/// 根据页面编码查询页面信息。
/// </summary>
/// <param name="webCode">页面编码</param>
/// <returns>页面实体</returns>
public async Task<HwWeb> SelectHwWebByWebcode(long webCode)
{
// 【匿名对象传参】
// new { webCode } 是 C# 的"匿名对象"语法。
// 编译器会自动创建一个包含 webCode 属性的临时类。
//
// 对比 Java
// Java 没有匿名对象语法,通常用:
// - Map&lt;String, Object&gt; params = new HashMap&lt;&gt;();
// - params.put("webCode", webCode);
// 或者:
// - @Param("webCode") Long webCodeMyBatis 注解)
//
// C# 的匿名对象更简洁,编译器自动推断类型。
// 回滚到 XML 方案时可直接恢复:
// return await _executor.QuerySingleAsync<HwWeb>(Mapper, "selectHwWebByWebcode", new { webCode });
return await _db.Queryable<HwWeb>()
.Where(item => item.IsDelete == "0" && item.WebCode == webCode)
.FirstAsync();
}
/// <summary>
/// 查询页面列表。
/// </summary>
/// <param name="input">查询条件</param>
/// <returns>页面列表</returns>
public async Task<List<HwWeb>> SelectHwWebList(HwWeb input)
{
// 【空合并运算符 ??】
// input ?? new HwWeb() 含义:
// 如果 input 不为 null就用 input
// 否则 new 一个默认的 HwWeb 对象。
//
// 对比 Java
// Java 需要手写:
// if (input == null) input = new HwWeb();
// 或者用 Optional
// input = Optional.ofNullable(input).orElse(new HwWeb());
HwWeb query = input ?? new HwWeb();
// 回滚到 XML 方案时可直接恢复:
// return await _executor.QueryListAsync<HwWeb>(Mapper, "selectHwWebList", query);
return await _db.Queryable<HwWeb>()
.Where(item => item.IsDelete == "0")
.WhereIF(query.WebId.HasValue, item => item.WebId == query.WebId)
.WhereIF(query.WebJson != null && query.WebJson != string.Empty, item => item.WebJson == query.WebJson)
.WhereIF(query.WebJsonString != null && query.WebJsonString != string.Empty, item => item.WebJsonString == query.WebJsonString)
.WhereIF(query.WebCode.HasValue, item => item.WebCode == query.WebCode)
.WhereIF(query.WebJsonEnglish != null, item => item.WebJsonEnglish == query.WebJsonEnglish)
.ToListAsync();
}
/// <summary>
/// 新增页面。
/// </summary>
/// <param name="input">页面数据</param>
/// <returns>影响行数1=成功0=失败)</returns>
public async Task<int> InsertHwWeb(HwWeb input)
{
// 【async/await 异步编程】
// await 会等待异步操作完成,然后继续执行后续代码。
//
// InsertReturnIdentityAsync 返回自增主键值。
// 对比 Java MyBatis
// Java MyBatis 需要配置 useGeneratedKeys="true" keyProperty="webId"
// 然后通过 input.getWebId() 获取主键。
input.IsDelete = string.IsNullOrWhiteSpace(input.IsDelete) ? "0" : input.IsDelete;
// 回滚到 XML 方案时可直接恢复:
// long identity = await _executor.InsertReturnIdentityAsync(Mapper, "insertHwWeb", input);
HwWeb entity = await _db.Insertable(input).ExecuteReturnEntityAsync();
// 把自增主键回填到实体对象。
// 这样调用方可以获取到新插入记录的 ID。
input.WebId = entity.WebId;
if (input.WebId > 0)
{
// 【静默重建搜索索引】
// 新增成功后,触发搜索索引重建。
// "静默"表示:索引重建失败不影响主业务,只记录日志。
await RebuildSearchIndexQuietly("hw_web");
}
// 返回 1 表示成功0 表示失败。
return input.WebId > 0 ? 1 : 0;
}
/// <summary>
/// 更新页面。
/// <para>
/// 【版本化更新策略】
/// 这里的更新不是传统 SQL UPDATE而是
/// 1. 按 webCode 找旧记录
/// 2. 逻辑删旧记录
/// 3. 插入新记录
///
/// 为什么这样做?
/// 1. 保留历史版本:旧记录还在,可以回滚
/// 2. 避免缓存失效:旧 ID 的缓存自然过期
/// 3. 保持引用稳定:其他表引用的是 webCode不是 webId
///
/// 这是一种"追加写入"的设计模式,适合需要审计追踪的场景。
/// </para>
/// </summary>
/// <param name="input">页面数据</param>
/// <returns>影响行数</returns>
public async Task<int> UpdateHwWeb(HwWeb input)
{
// 【对象初始化器】
// HwWeb query = new() { WebCode = input.WebCode };
// 等价于:
// HwWeb query = new HwWeb();
// query.WebCode = input.WebCode;
//
// new() 是 C# 9.0 的"目标类型 new"语法,编译器自动推断类型。
HwWeb query = new()
{
WebCode = input.WebCode
};
// 查询现有记录。
List<HwWeb> exists = await SelectHwWebList(query);
if (exists.Count > 0)
{
// 【LINQ 链式操作】
// exists.Where(...).Select(...).ToArray()
//
// Where筛选符合条件的元素
// Select转换元素这里是提取 WebId 值)
// ToArray转换为数组
//
// 对比 Java Stream
// exists.stream()
// .filter(u -> u.getWebId() != null)
// .map(u -> u.getWebId())
// .toArray(Long[]::new);
//
// C# 的 LINQ 和 Java Stream 非常相似。
//
// 【! 非空断言】
// u.WebId!.Value 中的 ! 是"非空断言"。
// 告诉编译器:我确定这里不为 null不要警告我。
// 因为 Where 条件已经过滤了 HasValue 为 false 的项。
await DeleteHwWebByWebIds(exists.Where(u => u.WebId.HasValue).Select(u => u.WebId!.Value).ToArray(), false);
}
// 确保新记录是有效状态is_delete = "0")。
input.IsDelete = "0";
// 回滚到 XML 方案时可直接恢复:
// long identity = await _executor.InsertReturnIdentityAsync(Mapper, "insertHwWeb", input);
HwWeb entity = await _db.Insertable(input).ExecuteReturnEntityAsync();
input.WebId = entity.WebId;
if (input.WebId > 0)
{
await RebuildSearchIndexQuietly("hw_web");
}
return input.WebId > 0 ? 1 : 0;
}
/// <summary>
/// 批量删除页面(公开方法,默认重建索引)。
/// </summary>
/// <param name="webIds">页面ID数组</param>
/// <returns>影响行数</returns>
public Task<int> DeleteHwWebByWebIds(long[] webIds)
{
// 这里故意再包一层公开方法,把"是否重建索引"的内部开关藏起来。
// 对外调用统一默认 true只有内部特殊场景才传 false。
return DeleteHwWebByWebIds(webIds, true);
}
/// <summary>
/// 批量删除页面(私有方法,可控制是否重建索引)。
/// </summary>
/// <param name="webIds">页面ID数组</param>
/// <param name="rebuild">是否重建搜索索引</param>
/// <returns>影响行数</returns>
private async Task<int> DeleteHwWebByWebIds(long[] webIds, bool rebuild)
{
// 执行删除 SQL。
// 回滚到 XML 方案时可直接恢复:
// int rows = await _executor.ExecuteAsync(Mapper, "deleteHwWebByWebIds", new { array = webIds });
List<HwWeb> pages = await _db.Queryable<HwWeb>()
.Where(item => item.WebId.HasValue && webIds.Contains(item.WebId.Value))
.ToListAsync();
foreach (HwWeb page in pages)
{
page.IsDelete = "1";
}
int rows = pages.Count == 0 ? 0 : await _db.Updateable(pages)
.UpdateColumns(item => new { item.IsDelete })
.ExecuteCommandAsync();
if (rows > 0 && rebuild)
{
// rebuild=false 的场景只出现在"更新前先删旧版本"这条内部链路。
// 那里最后会统一在新记录插入成功后重建一次索引,所以这里不能重复触发。
await RebuildSearchIndexQuietly("hw_web");
}
return rows;
}
/// <summary>
/// 静默重建搜索索引。
/// <para>
/// 【"静默"的含义】
/// "静默"Quietly表示索引重建失败不抛异常只记录日志。
///
/// 为什么静默?
/// 1. 搜索索引是附属能力:主业务(增删改)成功就够了
/// 2. 避免事务回滚:如果索引重建失败导致事务回滚,用户体验不好
/// 3. 可以后续修复:索引问题可以后台修复,不影响用户操作
///
/// 这是"最终一致性"的设计思想:
/// 主业务强一致,附属能力最终一致。
/// </para>
/// </summary>
/// <param name="source">触发重建的数据源(用于日志记录)</param>
private async Task RebuildSearchIndexQuietly(string source)
{
try
{
// 尝试重建所有搜索索引。
await _searchRebuildService.RebuildAllAsync();
}
catch (Exception ex)
{
// 【异常处理】
// 这里用 ILogger 记录错误日志,而不是 throw。
//
// _logger.LogError(ex, "message", args...) 参数说明:
// - ex异常对象会自动提取堆栈信息
// - "message":日志消息模板
// - args模板参数类似 String.Format
//
// 对比 Java SLF4J
// Java: log.error("rebuild portal search index failed after {} changed", source, ex);
// C#: _logger.LogError(ex, "rebuild portal search index failed after {Source} changed", source);
//
// C# 的消息模板用 {Source} 占位符Java 用 {}。
_logger.LogError(ex, "rebuild portal search index failed after {Source} changed", source);
}
}
}