464 lines
16 KiB
C#
464 lines
16 KiB
C#
using MiAssessment.Admin.Data;
|
||
using MiAssessment.Admin.Entities;
|
||
using MiAssessment.Admin.Models.Common;
|
||
using MiAssessment.Admin.Models.Dict;
|
||
using Microsoft.Data.SqlClient;
|
||
using Microsoft.EntityFrameworkCore;
|
||
using Microsoft.Extensions.Logging;
|
||
|
||
namespace MiAssessment.Admin.Services;
|
||
|
||
/// <summary>
|
||
/// 字典服务实现
|
||
/// </summary>
|
||
public class DictService : IDictService
|
||
{
|
||
private readonly AdminDbContext _dbContext;
|
||
private readonly ILogger<DictService> _logger;
|
||
|
||
/// <summary>
|
||
/// 数据源类型:静态数据
|
||
/// </summary>
|
||
private const byte SourceTypeStatic = 1;
|
||
|
||
/// <summary>
|
||
/// 数据源类型:SQL查询
|
||
/// </summary>
|
||
private const byte SourceTypeSql = 2;
|
||
|
||
/// <summary>
|
||
/// 状态:启用
|
||
/// </summary>
|
||
private const byte StatusEnabled = 1;
|
||
|
||
public DictService(AdminDbContext dbContext, ILogger<DictService> logger)
|
||
{
|
||
_dbContext = dbContext;
|
||
_logger = logger;
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<List<DictTypeDto>> GetTypesAsync()
|
||
{
|
||
return await _dbContext.DictTypes
|
||
.Where(t => t.Status == StatusEnabled)
|
||
.OrderBy(t => t.Sort)
|
||
.ThenBy(t => t.Id)
|
||
.Select(t => new DictTypeDto
|
||
{
|
||
Id = t.Id,
|
||
Code = t.Code,
|
||
Name = t.Name,
|
||
Description = t.Description,
|
||
SourceType = t.SourceType,
|
||
SourceSql = t.SourceSql,
|
||
Status = t.Status,
|
||
Sort = t.Sort,
|
||
CreatedAt = t.CreatedAt,
|
||
UpdatedAt = t.UpdatedAt
|
||
})
|
||
.ToListAsync();
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<DictTypeDto?> GetTypeByCodeAsync(string code)
|
||
{
|
||
var dictType = await _dbContext.DictTypes
|
||
.FirstOrDefaultAsync(t => t.Code == code);
|
||
|
||
if (dictType == null)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
return new DictTypeDto
|
||
{
|
||
Id = dictType.Id,
|
||
Code = dictType.Code,
|
||
Name = dictType.Name,
|
||
Description = dictType.Description,
|
||
SourceType = dictType.SourceType,
|
||
SourceSql = dictType.SourceSql,
|
||
Status = dictType.Status,
|
||
Sort = dictType.Sort,
|
||
CreatedAt = dictType.CreatedAt,
|
||
UpdatedAt = dictType.UpdatedAt
|
||
};
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<List<DictItemDto>> GetItemsByTypeCodeAsync(string typeCode)
|
||
{
|
||
// 获取字典类型
|
||
var dictType = await _dbContext.DictTypes
|
||
.FirstOrDefaultAsync(t => t.Code == typeCode);
|
||
|
||
if (dictType == null)
|
||
{
|
||
_logger.LogWarning("字典类型不存在: {TypeCode}", typeCode);
|
||
return new List<DictItemDto>();
|
||
}
|
||
|
||
// 检查字典类型是否禁用 (Requirements 4.6)
|
||
if (dictType.Status != StatusEnabled)
|
||
{
|
||
_logger.LogInformation("字典类型已禁用,不返回数据: {TypeCode}", typeCode);
|
||
return new List<DictItemDto>();
|
||
}
|
||
|
||
// 根据数据源类型返回数据
|
||
if (dictType.SourceType == SourceTypeStatic)
|
||
{
|
||
// 静态数据查询 (Requirements 4.4)
|
||
return await GetStaticItemsAsync(dictType.Id);
|
||
}
|
||
else if (dictType.SourceType == SourceTypeSql)
|
||
{
|
||
// 动态 SQL 查询 (Requirements 4.5)
|
||
return await GetDynamicItemsAsync(dictType);
|
||
}
|
||
|
||
_logger.LogWarning("未知的数据源类型: {SourceType}, TypeCode: {TypeCode}", dictType.SourceType, typeCode);
|
||
return new List<DictItemDto>();
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// 获取静态字典数据项 (Requirements 4.4, 4.7)
|
||
/// </summary>
|
||
private async Task<List<DictItemDto>> GetStaticItemsAsync(int typeId)
|
||
{
|
||
return await _dbContext.DictItems
|
||
.Where(i => i.TypeId == typeId && i.Status == StatusEnabled) // 过滤禁用项 (Requirements 4.7)
|
||
.OrderBy(i => i.Sort)
|
||
.ThenBy(i => i.Id)
|
||
.Select(i => new DictItemDto
|
||
{
|
||
Id = i.Id,
|
||
TypeId = i.TypeId,
|
||
Label = i.Label,
|
||
Value = i.Value,
|
||
Description = i.Description,
|
||
CssClass = i.CssClass,
|
||
Status = i.Status,
|
||
Sort = i.Sort,
|
||
CreatedAt = i.CreatedAt,
|
||
UpdatedAt = i.UpdatedAt
|
||
})
|
||
.ToListAsync();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 执行动态 SQL 查询获取字典数据 (Requirements 4.5)
|
||
/// SQL 查询必须返回 label 和 value 两列
|
||
/// </summary>
|
||
private async Task<List<DictItemDto>> GetDynamicItemsAsync(DictType dictType)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(dictType.SourceSql))
|
||
{
|
||
_logger.LogWarning("字典类型 {TypeCode} 的 SQL 查询语句为空", dictType.Code);
|
||
return new List<DictItemDto>();
|
||
}
|
||
|
||
var items = new List<DictItemDto>();
|
||
|
||
try
|
||
{
|
||
// 验证 SQL 安全性(只允许 SELECT 语句)
|
||
var sql = dictType.SourceSql.Trim();
|
||
if (!sql.StartsWith("SELECT", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
_logger.LogError("字典类型 {TypeCode} 的 SQL 不是 SELECT 语句,拒绝执行", dictType.Code);
|
||
return new List<DictItemDto>();
|
||
}
|
||
|
||
// 检查是否包含危险关键字
|
||
var dangerousKeywords = new[] { "INSERT", "UPDATE", "DELETE", "DROP", "TRUNCATE", "ALTER", "CREATE", "EXEC", "EXECUTE", "--", "/*" };
|
||
var upperSql = sql.ToUpperInvariant();
|
||
foreach (var keyword in dangerousKeywords)
|
||
{
|
||
if (upperSql.Contains(keyword))
|
||
{
|
||
_logger.LogError("字典类型 {TypeCode} 的 SQL 包含危险关键字 {Keyword},拒绝执行", dictType.Code, keyword);
|
||
return new List<DictItemDto>();
|
||
}
|
||
}
|
||
|
||
// 使用 ADO.NET 执行 SQL 查询
|
||
var connection = _dbContext.Database.GetDbConnection();
|
||
await connection.OpenAsync();
|
||
|
||
try
|
||
{
|
||
using var command = connection.CreateCommand();
|
||
command.CommandText = sql;
|
||
command.CommandTimeout = 30; // 30秒超时
|
||
|
||
using var reader = await command.ExecuteReaderAsync();
|
||
|
||
// 检查是否有 label 和 value 列
|
||
var hasLabel = false;
|
||
var hasValue = false;
|
||
for (int i = 0; i < reader.FieldCount; i++)
|
||
{
|
||
var columnName = reader.GetName(i).ToLowerInvariant();
|
||
if (columnName == "label") hasLabel = true;
|
||
if (columnName == "value") hasValue = true;
|
||
}
|
||
|
||
if (!hasLabel || !hasValue)
|
||
{
|
||
_logger.LogError("字典类型 {TypeCode} 的 SQL 查询结果必须包含 label 和 value 列", dictType.Code);
|
||
return new List<DictItemDto>();
|
||
}
|
||
|
||
var sortIndex = 0;
|
||
while (await reader.ReadAsync())
|
||
{
|
||
var item = new DictItemDto
|
||
{
|
||
Id = 0, // 动态数据没有 ID
|
||
TypeId = dictType.Id,
|
||
Label = reader["label"]?.ToString() ?? string.Empty,
|
||
Value = reader["value"]?.ToString() ?? string.Empty,
|
||
Status = StatusEnabled,
|
||
Sort = sortIndex++,
|
||
CreatedAt = DateTime.Now
|
||
};
|
||
|
||
// 尝试读取可选列
|
||
try
|
||
{
|
||
var descOrdinal = reader.GetOrdinal("description");
|
||
if (!reader.IsDBNull(descOrdinal))
|
||
{
|
||
item.Description = reader.GetString(descOrdinal);
|
||
}
|
||
}
|
||
catch { /* 列不存在,忽略 */ }
|
||
|
||
try
|
||
{
|
||
var cssOrdinal = reader.GetOrdinal("css_class");
|
||
if (!reader.IsDBNull(cssOrdinal))
|
||
{
|
||
item.CssClass = reader.GetString(cssOrdinal);
|
||
}
|
||
}
|
||
catch { /* 列不存在,忽略 */ }
|
||
|
||
items.Add(item);
|
||
}
|
||
}
|
||
finally
|
||
{
|
||
if (connection.State == System.Data.ConnectionState.Open)
|
||
{
|
||
await connection.CloseAsync();
|
||
}
|
||
}
|
||
|
||
_logger.LogInformation("字典类型 {TypeCode} 动态 SQL 查询成功,返回 {Count} 条数据", dictType.Code, items.Count);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "执行字典类型 {TypeCode} 的动态 SQL 查询失败", dictType.Code);
|
||
}
|
||
|
||
return items;
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<DictTypeDto> CreateTypeAsync(CreateDictTypeRequest request)
|
||
{
|
||
// 检查编码是否重复
|
||
var codeExists = await _dbContext.DictTypes.AnyAsync(t => t.Code == request.Code);
|
||
if (codeExists)
|
||
{
|
||
throw new AdminException(AdminErrorCodes.DuplicateDictTypeCode, "字典编码已存在");
|
||
}
|
||
|
||
// 验证 SQL 查询类型必须提供 SQL 语句
|
||
if (request.SourceType == SourceTypeSql && string.IsNullOrWhiteSpace(request.SourceSql))
|
||
{
|
||
throw new AdminException(AdminErrorCodes.InvalidParameter, "SQL查询类型必须提供SQL语句");
|
||
}
|
||
|
||
var dictType = new DictType
|
||
{
|
||
Code = request.Code,
|
||
Name = request.Name,
|
||
Description = request.Description,
|
||
SourceType = request.SourceType,
|
||
SourceSql = request.SourceSql,
|
||
Status = request.Status,
|
||
Sort = request.Sort,
|
||
CreatedAt = DateTime.Now
|
||
};
|
||
|
||
_dbContext.DictTypes.Add(dictType);
|
||
await _dbContext.SaveChangesAsync();
|
||
|
||
_logger.LogInformation("创建字典类型成功: {DictTypeId} - {DictTypeCode}", dictType.Id, dictType.Code);
|
||
|
||
return new DictTypeDto
|
||
{
|
||
Id = dictType.Id,
|
||
Code = dictType.Code,
|
||
Name = dictType.Name,
|
||
Description = dictType.Description,
|
||
SourceType = dictType.SourceType,
|
||
SourceSql = dictType.SourceSql,
|
||
Status = dictType.Status,
|
||
Sort = dictType.Sort,
|
||
CreatedAt = dictType.CreatedAt,
|
||
UpdatedAt = dictType.UpdatedAt
|
||
};
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<bool> UpdateTypeAsync(int id, UpdateDictTypeRequest request)
|
||
{
|
||
var dictType = await _dbContext.DictTypes.FindAsync(id);
|
||
if (dictType == null)
|
||
{
|
||
throw new AdminException(AdminErrorCodes.DictTypeNotFound, "字典类型不存在");
|
||
}
|
||
|
||
// 检查编码是否重复(排除自己)
|
||
var codeExists = await _dbContext.DictTypes.AnyAsync(t => t.Code == request.Code && t.Id != id);
|
||
if (codeExists)
|
||
{
|
||
throw new AdminException(AdminErrorCodes.DuplicateDictTypeCode, "字典编码已存在");
|
||
}
|
||
|
||
// 验证 SQL 查询类型必须提供 SQL 语句
|
||
if (request.SourceType == SourceTypeSql && string.IsNullOrWhiteSpace(request.SourceSql))
|
||
{
|
||
throw new AdminException(AdminErrorCodes.InvalidParameter, "SQL查询类型必须提供SQL语句");
|
||
}
|
||
|
||
dictType.Code = request.Code;
|
||
dictType.Name = request.Name;
|
||
dictType.Description = request.Description;
|
||
dictType.SourceType = request.SourceType;
|
||
dictType.SourceSql = request.SourceSql;
|
||
dictType.Status = request.Status;
|
||
dictType.Sort = request.Sort;
|
||
dictType.UpdatedAt = DateTime.Now;
|
||
|
||
await _dbContext.SaveChangesAsync();
|
||
|
||
_logger.LogInformation("更新字典类型成功: {DictTypeId} - {DictTypeCode}", dictType.Id, dictType.Code);
|
||
return true;
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<bool> DeleteTypeAsync(int id)
|
||
{
|
||
var dictType = await _dbContext.DictTypes.FindAsync(id);
|
||
if (dictType == null)
|
||
{
|
||
throw new AdminException(AdminErrorCodes.DictTypeNotFound, "字典类型不存在");
|
||
}
|
||
|
||
// 删除关联的字典数据项
|
||
var items = await _dbContext.DictItems.Where(i => i.TypeId == id).ToListAsync();
|
||
_dbContext.DictItems.RemoveRange(items);
|
||
|
||
_dbContext.DictTypes.Remove(dictType);
|
||
await _dbContext.SaveChangesAsync();
|
||
|
||
_logger.LogInformation("删除字典类型成功: {DictTypeId} - {DictTypeCode},同时删除 {ItemCount} 条数据项",
|
||
id, dictType.Code, items.Count);
|
||
return true;
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<DictItemDto> CreateItemAsync(CreateDictItemRequest request)
|
||
{
|
||
// 检查字典类型是否存在
|
||
var dictType = await _dbContext.DictTypes.FindAsync(request.TypeId);
|
||
if (dictType == null)
|
||
{
|
||
throw new AdminException(AdminErrorCodes.DictTypeNotFound, "字典类型不存在");
|
||
}
|
||
|
||
// 检查字典类型是否为静态数据类型
|
||
if (dictType.SourceType != SourceTypeStatic)
|
||
{
|
||
throw new AdminException(AdminErrorCodes.InvalidParameter, "只有静态数据类型的字典才能添加数据项");
|
||
}
|
||
|
||
var dictItem = new DictItem
|
||
{
|
||
TypeId = request.TypeId,
|
||
Label = request.Label,
|
||
Value = request.Value,
|
||
Description = request.Description,
|
||
CssClass = request.CssClass,
|
||
Status = request.Status,
|
||
Sort = request.Sort,
|
||
CreatedAt = DateTime.Now
|
||
};
|
||
|
||
_dbContext.DictItems.Add(dictItem);
|
||
await _dbContext.SaveChangesAsync();
|
||
|
||
_logger.LogInformation("创建字典数据项成功: {DictItemId} - {Label}", dictItem.Id, dictItem.Label);
|
||
|
||
return new DictItemDto
|
||
{
|
||
Id = dictItem.Id,
|
||
TypeId = dictItem.TypeId,
|
||
Label = dictItem.Label,
|
||
Value = dictItem.Value,
|
||
Description = dictItem.Description,
|
||
CssClass = dictItem.CssClass,
|
||
Status = dictItem.Status,
|
||
Sort = dictItem.Sort,
|
||
CreatedAt = dictItem.CreatedAt,
|
||
UpdatedAt = dictItem.UpdatedAt
|
||
};
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<bool> UpdateItemAsync(int id, UpdateDictItemRequest request)
|
||
{
|
||
var dictItem = await _dbContext.DictItems.FindAsync(id);
|
||
if (dictItem == null)
|
||
{
|
||
throw new AdminException(AdminErrorCodes.DictItemNotFound, "字典数据项不存在");
|
||
}
|
||
|
||
dictItem.Label = request.Label;
|
||
dictItem.Value = request.Value;
|
||
dictItem.Description = request.Description;
|
||
dictItem.CssClass = request.CssClass;
|
||
dictItem.Status = request.Status;
|
||
dictItem.Sort = request.Sort;
|
||
dictItem.UpdatedAt = DateTime.Now;
|
||
|
||
await _dbContext.SaveChangesAsync();
|
||
|
||
_logger.LogInformation("更新字典数据项成功: {DictItemId} - {Label}", dictItem.Id, dictItem.Label);
|
||
return true;
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<bool> DeleteItemAsync(int id)
|
||
{
|
||
var dictItem = await _dbContext.DictItems.FindAsync(id);
|
||
if (dictItem == null)
|
||
{
|
||
throw new AdminException(AdminErrorCodes.DictItemNotFound, "字典数据项不存在");
|
||
}
|
||
|
||
_dbContext.DictItems.Remove(dictItem);
|
||
await _dbContext.SaveChangesAsync();
|
||
|
||
_logger.LogInformation("删除字典数据项成功: {DictItemId} - {Label}", id, dictItem.Label);
|
||
return true;
|
||
}
|
||
}
|