admin/src/Oms.Application/Services/System/DictService.cs
zzh b17e6639a4
All checks were successful
Build and Deploy / deploy (push) Successful in 1m3s
fix: 主数据新增兼容软删唯一键,避免 Duplicate entry
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-05 11:53:08 +08:00

337 lines
12 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 System.Linq.Expressions;
using Fake.Application;
using Microsoft.AspNetCore.Mvc;
using Oms.Application.Dtos.Shared;
using Oms.Application.Dtos.System;
using Oms.Application.Shared;
using Oms.Domain.SystemAggregate.Entities;
using Oms.Domain.SystemAggregate.Repositories;
namespace Oms.Application.Services.System;
/// <summary>
/// 字典 API 服务。
/// </summary>
[ApiExplorerSettings(GroupName = "OMS-System")]
public class DictService : ApplicationService
{
private IDictRepository DictRepository => LazyServiceProvider.GetRequiredService<IDictRepository>();
private IDictTypeRepository DictTypeRepository => LazyServiceProvider.GetRequiredService<IDictTypeRepository>();
/// <summary>
/// 查询字典列表。
/// </summary>
/// <param name="input">入参:字典分页查询条件。</param>
/// <returns>字典分页结果,包含字典列表与总记录数。</returns>
[HttpGet("/system/dict/list")]
public async Task<OmsPageResult<DictOutput>> GetListAsync([FromQuery] DictListInput input)
{
var predicate = BuildListPredicate(input);
var pageIndex = OmsDocHelper.NormalizePage(input.Page);
var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize);
// Fake GetPagedListAsync(sorting) 会丢失 Where:total 正确但 items 返回全量
var (items, total) = await RepositoryPageHelper.GetPageAsync(
DictRepository,
predicate,
pageIndex,
pageSize,
q => q.OrderBy(x => x.DictSort).ThenByDescending(x => x.Id));
return new OmsPageResult<DictOutput>
{
Items = items.Select(MapDictOutput).ToList(),
Total = total
};
}
/// <summary>
/// 查询字典类型列表(含备注与项数)。
/// </summary>
/// <param name="keyword">可选:按类型编码或备注模糊搜索。</param>
/// <returns>字典类型列表。</returns>
[HttpGet("/system/dict/type/list")]
public async Task<List<DictTypeOutput>> GetTypeListAsync([FromQuery] string? keyword)
{
var kw = keyword?.Trim();
var metaRows = await DictTypeRepository.GetListAsync();
var countRows = await DictRepository.GetItemCountByTypeAsync();
var countMap = countRows
.Select(x => (Type: x.DictType.Trim(), x.ItemCount))
.Where(x => !string.IsNullOrWhiteSpace(x.Type))
.GroupBy(x => x.Type, StringComparer.OrdinalIgnoreCase)
.ToDictionary(g => g.Key, g => g.Sum(x => x.ItemCount), StringComparer.OrdinalIgnoreCase);
var merged = new Dictionary<string, DictTypeOutput>(StringComparer.OrdinalIgnoreCase);
foreach (var meta in metaRows)
{
var type = meta.DictType.Trim();
if (string.IsNullOrWhiteSpace(type))
{
continue;
}
merged[type] = new DictTypeOutput
{
Id = meta.Id,
DictType = type,
Remark = meta.Remark,
TypeSort = meta.TypeSort,
ItemCount = countMap.TryGetValue(type, out var count) ? count : 0
};
}
foreach (var (dictType, itemCount) in countRows)
{
var type = dictType.Trim();
if (string.IsNullOrWhiteSpace(type) || merged.ContainsKey(type))
{
continue;
}
merged[type] = new DictTypeOutput
{
DictType = type,
ItemCount = itemCount
};
}
IEnumerable<DictTypeOutput> result = merged.Values;
if (!string.IsNullOrWhiteSpace(kw))
{
result = result.Where(x =>
x.DictType.Contains(kw, StringComparison.OrdinalIgnoreCase) ||
(!string.IsNullOrWhiteSpace(x.Remark) && x.Remark.Contains(kw, StringComparison.OrdinalIgnoreCase)));
}
return result
.OrderBy(x => x.TypeSort)
.ThenBy(x => x.DictType, StringComparer.OrdinalIgnoreCase)
.ToList();
}
/// <summary>
/// 保存字典类型元数据(新增或更新备注)。
/// </summary>
/// <param name="input">入参:字典类型元数据。</param>
/// <returns>无返回体。</returns>
[HttpPost("/system/dict/type/save")]
public async Task SaveTypeAsync(DictTypeSaveInput input)
{
var dictType = input.DictType?.Trim() ?? string.Empty;
if (string.IsNullOrWhiteSpace(dictType))
{
throw new BusinessException("字典类型不能为空");
}
var remark = string.IsNullOrWhiteSpace(input.Remark) ? null : input.Remark.Trim();
DictTypeMeta? entity = null;
if (input.Id.HasValue && input.Id.Value > 0)
{
entity = await DictTypeRepository.FirstOrDefaultAsync(x => x.Id == input.Id.Value);
}
entity ??= await DictTypeRepository.GetByDictTypeAsync(dictType);
// 软删后列表查不到,但 uk_dict_type_code 仍占坑;重建时恢复原行
entity ??= await DictTypeRepository.GetByDictTypeIncludingDeletedAsync(dictType);
if (entity == null)
{
await DictTypeRepository.InsertAsync(new DictTypeMeta
{
DictType = dictType,
Remark = remark,
TypeSort = input.TypeSort
});
return;
}
if (!string.Equals(entity.DictType, dictType, StringComparison.OrdinalIgnoreCase))
{
var duplicate = await DictTypeRepository.GetByDictTypeIncludingDeletedAsync(dictType);
if (duplicate != null && duplicate.Id != entity.Id && !duplicate.IsDeleted)
{
throw new BusinessException("字典类型已存在");
}
if (duplicate != null && duplicate.Id != entity.Id && duplicate.IsDeleted)
{
throw new BusinessException("字典类型编码与已删除记录冲突,请换编码或联系管理员恢复");
}
}
entity.IsDeleted = false;
entity.DictType = dictType;
entity.Remark = remark;
entity.TypeSort = input.TypeSort;
await DictTypeRepository.UpdateAsync(entity);
}
/// <summary>
/// 删除字典类型元数据(不删除字典项)。
/// </summary>
/// <param name="input">入参:目标主键。</param>
/// <returns>无返回体。</returns>
[HttpPost("/system/dict/type/delete")]
public async Task DeleteTypeAsync(IdInput input)
{
var entity = await DictTypeRepository.FirstOrDefaultAsync(x => x.Id == input.Id);
if (entity == null)
{
throw new BusinessException("字典类型不存在");
}
await DictTypeRepository.DeleteAsync(entity);
}
/// <summary>
/// 新增字典。
/// </summary>
/// <param name="input">入参:字典新增参数。</param>
/// <returns>无返回体,仅完成新增字典。</returns>
[HttpPost("/system/dict/create")]
public async Task CreateAsync(DictCreateInput input)
{
var dictType = input.DictType.Trim();
var dictCode = input.DictCode.Trim();
var dictName = input.DictName.Trim();
await ValidateDictInputAsync(dictType, dictCode, dictName, null);
await EnsureDictTypeMetaAsync(dictType);
var deleted = await DictRepository.GetByTypeAndCodeIncludingDeletedAsync(dictType, dictCode);
if (deleted != null && deleted.IsDeleted)
{
deleted.IsDeleted = false;
deleted.DictSort = input.DictSort;
deleted.DictName = dictName;
await DictRepository.UpdateAsync(deleted);
return;
}
await DictRepository.InsertAsync(new Dict
{
DictType = dictType,
DictSort = input.DictSort,
DictCode = dictCode,
DictName = dictName
});
}
/// <summary>
/// 更新字典。
/// </summary>
/// <param name="input">入参:字典更新参数。</param>
/// <returns>无返回体,仅完成更新字典。</returns>
[HttpPost("/system/dict/update")]
public async Task UpdateAsync(DictUpdateInput input)
{
var entity = await GetDictOrThrowAsync(input.Id);
await ValidateDictInputAsync(input.DictType, input.DictCode, input.DictName, entity.Id);
await EnsureDictTypeMetaAsync(input.DictType.Trim());
entity.DictType = input.DictType.Trim();
entity.DictSort = input.DictSort;
entity.DictCode = input.DictCode.Trim();
entity.DictName = input.DictName.Trim();
await DictRepository.UpdateAsync(entity);
}
/// <summary>
/// 删除字典。
/// </summary>
/// <param name="input">入参:目标主键。</param>
/// <returns>无返回体,仅完成删除字典。</returns>
[HttpPost("/system/dict/delete")]
public async Task DeleteAsync(IdInput input)
{
var entity = await GetDictOrThrowAsync(input.Id);
await DictRepository.DeleteAsync(entity);
}
private static Expression<Func<Dict, bool>> BuildListPredicate(DictListInput input)
{
var dictType = input.DictType?.Trim();
var keyword = input.Keyword?.Trim();
return x =>
(string.IsNullOrWhiteSpace(dictType) || x.DictType == dictType) &&
(string.IsNullOrWhiteSpace(keyword) ||
x.DictCode.Contains(keyword) ||
x.DictName.Contains(keyword));
}
private async Task<Dict> GetDictOrThrowAsync(int id)
{
var entity = await DictRepository.FirstOrDefaultAsync(x => x.Id == id);
if (entity == null)
{
throw new BusinessException("字典不存在");
}
return entity;
}
private async Task ValidateDictInputAsync(string dictType, string dictCode, string dictName, int? excludeId)
{
if (string.IsNullOrWhiteSpace(dictType))
{
throw new BusinessException("字典类型不能为空");
}
if (string.IsNullOrWhiteSpace(dictCode))
{
throw new BusinessException("字典编码不能为空");
}
if (string.IsNullOrWhiteSpace(dictName))
{
throw new BusinessException("字典名称不能为空");
}
var existed = await DictRepository.GetByTypeAndCodeAsync(dictType.Trim(), dictCode.Trim());
if (existed != null && existed.Id != excludeId)
{
throw new BusinessException("同类型下字典编码已存在");
}
}
private static DictOutput MapDictOutput(Dict entity)
{
return new DictOutput
{
Id = entity.Id,
DictType = entity.DictType,
DictSort = entity.DictSort,
DictCode = entity.DictCode,
DictName = entity.DictName,
CreateTime = entity.CreateTime,
UpdateTime = OmsDocHelper.NullIfDefault(entity.UpdateTime)
};
}
private async Task EnsureDictTypeMetaAsync(string dictType)
{
if (string.IsNullOrWhiteSpace(dictType))
{
return;
}
var existed = await DictTypeRepository.GetByDictTypeAsync(dictType);
if (existed != null)
{
return;
}
var deleted = await DictTypeRepository.GetByDictTypeIncludingDeletedAsync(dictType);
if (deleted != null)
{
deleted.IsDeleted = false;
await DictTypeRepository.UpdateAsync(deleted);
return;
}
await DictTypeRepository.InsertAsync(new DictTypeMeta
{
DictType = dictType,
TypeSort = 0
});
}
}