mi-assessment/server/MiAssessment/src/MiAssessment.Admin/Services/DictService.cs
2026-02-03 14:25:01 +08:00

464 lines
16 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}
}