|
|
// ============================================================================
|
|
|
// 【文件说明】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<HwWebService> 是 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<HwWebService> _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<String, Object> params = new HashMap<>();
|
|
|
// - params.put("webCode", webCode);
|
|
|
// 或者:
|
|
|
// - @Param("webCode") Long webCode(MyBatis 注解)
|
|
|
//
|
|
|
// 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);
|
|
|
}
|
|
|
}
|
|
|
}
|