using Mds.Customization.CodeRule;
using Mds.Master.Entities;
using Mds.Master.Operation;
using Mds.Master.Repositories;
using Mds.Toolkit;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Volo.Abp;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Domain.Entities;
using Volo.Abp.Domain.Repositories;
namespace Mds.Master
{
public class MasterOperationService(
IOperationRepository operationRepo,
IRepository<SubOperationEntity, Guid> subOperationRepo
) : MdsCrudAppService<
OperationEntity,
OperationGetOutputDto,
Dictionary<string, object>,
Guid,
OperationGetListInput,
OperationCreateInput,
OperationUpdateInput
>(operationRepo), IMasterOperationService
{
public async override Task<OperationGetOutputDto> CreateAsync(OperationCreateInput input)
{
if (string.IsNullOrWhiteSpace(input.OperationCode))
{
string code = await CodeService.GenCodeAsync(CodeRuleConstant.OperationCodeRule);
input.OperationCode = code;
}
await base.EnsureEntityUniqueAsync(GuidGenerator.Create(), input, "OperationName");
return await base.CreateAsync(input);
}
public async override Task<OperationGetOutputDto> UpdateAsync(Guid id, OperationUpdateInput input)
{
if (string.IsNullOrWhiteSpace(input.OperationCode))
{
string code = await CodeService.GenCodeAsync(CodeRuleConstant.OperationCodeRule);
input.OperationCode = code;
}
await base.EnsureEntityUniqueAsync(id, input, "OperationName");
return await base.UpdateAsync(id, input);
}
#region 外键关联代码
protected override void MapToEntity(OperationUpdateInput updateInput, OperationEntity entity)
{
base.MapToEntity(updateInput, entity);
SetIdForGuids(entity);
}
protected override void SetIdForGuids(OperationEntity entity)
{
base.SetIdForGuids(entity);
// 1. 筛选当前主工序下的所有子工序(同一层级,保持原始顺序)
var subOperations = entity.SubOperations
.Where(sub => sub.OperationId == entity.Id)
.ToList().OrderBy(sub => sub.StepSequence);
// 2. 处理子工序(不排序,保留原始顺序和StepSequence)
foreach (var subOp in subOperations)
{
// 原有逻辑:生成子工序ID + 绑定外键(保留)
if (subOp.Id == Guid.Empty)
EntityHelper.TrySetId(subOp, () => GuidGenerator.Create(), true);
subOp.OperationId = entity.Id;
// 原有校验:子工序父编码不能等于主工序编码(保留): Todo: 这里需要在用户编辑页面时就处理,而且这里是多级的不是单级,递归的项目不应该显示在用户的选择项中。
if (subOp.SubOperationId == entity.Id)
{
throw new BusinessException(MdsDomainErrorCodes.OperationCheck);
}
// 移除了StepSequence的重排逻辑,保留原始值
}
// 资源和收费项外键处理(保持不变)
foreach (var item in entity.OperationResource)
{
if (item.Id == Guid.Empty)
EntityHelper.TrySetId(item, () => GuidGenerator.Create(), true);
item.OperationId = entity.Id;
}
foreach (var item in entity.OperationChargeItems)
{
if (item.Id == Guid.Empty)
EntityHelper.TrySetId(item, () => GuidGenerator.Create(), true);
item.OperationId = entity.Id;
}
}
public async Task<TreeResponseDto<OperationNodeDto>> GetOperationById(OperationGetListInput input)
{
// 1. 入参校验
if (input.OperationCode.IsNullOrEmpty())
{
return new TreeResponseDto<OperationNodeDto>
{
TotalCount = 0,
Items = new()
};
}
// 2. 筛选主工序(按Code前缀)并分页
var opQry = await operationRepo.GetQueryableAsync();
var mainOpQuery = opQry
.Where(op => op.OperationCode.StartsWith(input.OperationCode))
.OrderBy(op => op.OperationCode);
var totalMainOpCount = await AsyncExecuter.CountAsync(mainOpQuery);
var pagedMainOpIds = await mainOpQuery
.Skip(input.SkipCount)
.Take(input.MaxResultCount)
.Select(op => op.Id)
.ToListAsync();
if (!pagedMainOpIds.Any())
{
return new TreeResponseDto<OperationNodeDto>
{
TotalCount = 0,
Items = new()
};
}
// 3. 调用原有树形逻辑
var nestedTrees = await GetNestedTreeAsync(pagedMainOpIds);
// 4. 关键:递归拉平嵌套节点,所有节点作为顶级节点
var flatNodes = new List<OperationNodeDto>();
var queue = new Queue<OperationNodeDto>(nestedTrees); // 初始队列放根节点
while (queue.Count > 0)
{
var current = queue.Dequeue();
// 克隆当前节点,清空children(避免保留嵌套关系)
var flatNode = new OperationNodeDto
{
Id = current.Id,
OperationType = current.OperationType,
OperationCode = current.OperationCode,
OperationName = current.OperationName,
Description = current.Description,
StandardDuration = current.StandardDuration,
IndirectlyHours = current.IndirectlyHours,
FixedHours = current.FixedHours,
RequiredQc = current.RequiredQc,
IsInActive = current.IsInActive,
RecordTypeId = current.RecordTypeId,
ParentCode = current.ParentCode, // 保留父编码信息
Level = current.Level,
StepSequence = current.StepSequence,
ParentId = current.ParentId, // 保留父ID信息
Children = new List<OperationNodeDto>() // 强制子节点为空
};
flatNodes.Add(flatNode);
// 将子节点入队,继续拉平(递归处理所有层级)
foreach (var child in current.Children)
{
queue.Enqueue(child);
}
}
// 5. 去重(按ID唯一)
var uniqueFlatNodes = flatNodes
.GroupBy(node => node.Id)
.Select(group => group.First())
.ToList();
// 6. 返回拉平后的结果
return new TreeResponseDto<OperationNodeDto>
{
TotalCount = uniqueFlatNodes.Count, // 总节点数(主+所有子节点)
Items = uniqueFlatNodes // 所有节点均为顶级,无嵌套
};
}
#endregion
#region 查询Operation Tree数据方法
public Task<List<OperationNodeFlatDto>> GetFlatTreeAsync(IEnumerable<Guid> rootIds)
=> GetFlatNodesAsync(rootIds);
public Task<List<OperationNodeFlatDto>> GetFlatTreeAsync(Guid rootId)
=> GetFlatNodesAsync(new[] { rootId }).ContinueWith(t => t.Result.ToList());
public Task<OperationNodeDto?> GetNestedTreeAsync(Guid rootId)
=> GetNestedTreeAsync(new[] { rootId }).ContinueWith(t => t.Result.FirstOrDefault());
//#region 修改查询方法(返回前端所需格式)
//// 1. 修改 GetAllOperationsTreeAsync 方法的返回类型为 TreeResponseDto<OperationNodeDto>
//public async Task<TreeResponseDto<OperationNodeDto>> GetAllOperationsTreeAsync()
//{
// var opQry = await operationRepo.GetQueryableAsync();
// var allOperationIds = await opQry
// .Select(o => o.Id)
// .ToListAsync();
// // 获取原始嵌套树形数据
// var nestedTrees = await GetNestedTreeAsync(allOperationIds);
// // 2. 根节点去重(按 Id 唯一,避免同一节点多次作为根节点)
// var uniqueRoots = nestedTrees
// .GroupBy(node => node.Id) // 按节点唯一标识分组
// .Select(group => group.First()) // 每组只保留第一个节点(去重)
// .ToList();
// // 3. 包装为前端所需格式
// return new TreeResponseDto<OperationNodeDto>
// {
// TotalCount = uniqueRoots.Count, // 总根节点数量(可根据需求改为总节点数)
// Items = uniqueRoots // 去重后的嵌套根节点数组
// };
//}
//#endregion
#region 修改查询方法(返回前端所需格式)
// 1. 修改 GetAllOperationsTreeAsync 方法的返回类型为 TreeResponseDto<OperationNodeDto>
public async Task<TreeResponseDto<OperationNodeDto>> GetAllOperationsTreeAsync()
{
var opQry = await operationRepo.GetQueryableAsync();
var allOperationIds = await opQry
.Select(o => o.Id)
.ToListAsync();
// 获取原始嵌套树形数据
var nestedTrees = await GetNestedTreeAsync(allOperationIds);
// 2. 根节点去重(按 Id 唯一,避免同一节点多次作为根节点)
var uniqueRoots = nestedTrees
.GroupBy(node => node.Id) // 按节点唯一标识分组
.Select(group => group.First()) // 每组只保留第一个节点(去重)
.ToList();
// 3. 关键:只保留一级根节点中有子工序的节点(Children 不为空)
var filteredRoots = uniqueRoots
.Where(root => root.Children != null && root.Children.Any())
.ToList();
// 4. 包装为前端所需格式
return new TreeResponseDto<OperationNodeDto>
{
TotalCount = filteredRoots.Count, // 过滤后的一级根节点数量
Items = filteredRoots // 仅保留有子工序的一级根节点,其子节点正常保留(无论是否有子工序)
};
}
#endregion
public async Task<List<OperationNodeDto>> GetNestedTreeAsync(IEnumerable<Guid> rootIds)
{
var flat = await GetFlatNodesAsync(rootIds);
if (!flat.Any()) return new();
var roots = new List<OperationNodeDto>();
var nodeById = new Dictionary<Guid, List<OperationNodeDto>>(); // 改成 List!
foreach (var item in flat)
{
var node = new OperationNodeDto
{
Id = item.Id,
OperationType = item.OperationType,
OperationCode = item.OperationCode,
OperationName = item.OperationName,
Description = item.Description,
StandardDuration = item.StandardDuration,
IndirectlyHours = item.IndirectlyHours,
FixedHours = item.FixedHours,
RequiredQc = item.RequiredQc,
IsInActive = item.IsInActive,
RecordTypeId = item.RecordTypeId,
ParentCode = item.ParentCode,
Level = item.Level,
StepSequence = item.StepSequence,
ParentId = item.ParentId,
Children = new List<OperationNodeDto>()
};
// 同一个 Id 可以有多个实例(每条路径一个)
if (!nodeById.ContainsKey(item.Id))
nodeById[item.Id] = new List<OperationNodeDto>();
nodeById[item.Id].Add(node);
if (item.ParentId == null)
{
roots.Add(node);
}
else
{
// 找到这个父节点的所有实例,子节点挂到所有父实例下(可选)
if (nodeById.TryGetValue(item.ParentId.Value, out var parentInstances))
{
foreach (var parent in parentInstances)
{
// 可选:避免重复添加
if (!parent.Children.Any(c => c.Id == node.Id && c.ParentId == item.ParentId))
{
parent.Children.Add(node);
}
}
}
}
}
return roots;
}
#endregion
#region 私有方法
private async Task<List<OperationNodeFlatDto>> GetFlatNodesAsync(IEnumerable<Guid> rootIds)
{
var rootIdList = rootIds?.Where(id => id != Guid.Empty).Distinct().ToHashSet();
if (rootIdList == null || rootIdList.Count == 0)
return new();
var opQry = await operationRepo.GetQueryableAsync();
var subOpQry = await subOperationRepo.GetQueryableAsync();
// 1. 收集所有后代 Id
var allIds = new HashSet<Guid>(rootIdList);
var toProcess = new Queue<Guid>(rootIdList);
while (toProcess.Count > 0)
{
var batch = toProcess.Take(500).ToList();
toProcess = new Queue<Guid>(toProcess.Skip(500));
var childIds = await subOpQry
.Where(s => batch.Contains(s.OperationId))
.Select(s => s.SubOperationId)
.ToListAsync();
foreach (var childId in childIds)
if (allIds.Add(childId))
toProcess.Enqueue(childId);
}
// 2. 加载所有节点和所有边
var operations = await opQry
.Where(o => allIds.Contains(o.Id))
.ToListAsync();
var idToCodeMap = operations.ToDictionary(op => op.Id, op => op.OperationCode);
var links = await subOpQry
.Where(s => allIds.Contains(s.OperationId) && allIds.Contains(s.SubOperationId))
.OrderBy(s => s.OperationId)
.ThenBy(s => s.StepSequence)
.ToListAsync();
// 3. 关键:不再用 Dictionary 去重!直接生成多条记录
var result = new List<OperationNodeFlatDto>();
// 用于计算 Level 和路径
var nodeInfo = new Dictionary<Guid, (int Level, Guid? ParentId, int StepSequence)>();
var queue = new Queue<(Guid Id, int Level, Guid? ParentId, int StepSequence)>();
// 初始化根节点
foreach (var rootId in rootIdList)
{
if (operations.Any(o => o.Id == rootId))
{
queue.Enqueue((rootId, 0, null, 0));
nodeInfo[rootId] = (0, null, 0);
}
}
while (queue.Count > 0)
{
var (currentId, level, parentId, seq) = queue.Dequeue();
var op = operations.First(o => o.Id == currentId);
var parentCode = parentId.HasValue
? idToCodeMap.TryGetValue(parentId.Value, out var code) ? code : null
: null;
// 每出现一次,就生成一条记录(支持多父!)
result.Add(new OperationNodeFlatDto
{
Id = op.Id,
OperationType = op.OperationType,
OperationCode = op.OperationCode,
OperationName = op.OperationName,
Description = op.Description,
StandardDuration = op.StandardDuration,
IndirectlyHours = op.IndirectlyHours,
FixedHours = op.FixedHours,
RequiredQc = op.RequiredQc,
IsInActive = op.IsInActive,
RecordTypeId = op.RecordTypeId,
ParentCode = parentCode,
ParentId = parentId,
Level = level,
StepSequence = seq
});
// 查找当前节点的所有子边
var children = links.Where(l => l.OperationId == currentId).ToList();
foreach (var child in children)
{
var childId = child.SubOperationId;
var childLevel = level + 1;
// 即使 childId 已出现过,也要再入队!(支持多路径)
queue.Enqueue((childId, childLevel, currentId, child.StepSequence));
}
}
return result;
}
#endregion
// 在 MasterOperationService 中添加
/// <summary>
/// 计算指定工序ID集合的层级(根节点0,子节点=父节点+1)
/// </summary>
private async Task<Dictionary<Guid, int>> CalculateOperationLevelsAsync(IEnumerable<Guid> operationIds)
{
var levelMap = new Dictionary<Guid, int>();
if (!operationIds.Any()) return levelMap;
// 1. 获取所有相关的父子关系(父OperationId -> 子SubOperationId)
var subOpQry = await subOperationRepo.GetQueryableAsync();
var parentChildRelations = await subOpQry
.Where(s => operationIds.Contains(s.OperationId) || operationIds.Contains(s.SubOperationId))
.Select(s => new { ParentId = s.OperationId, ChildId = s.SubOperationId })
.ToListAsync();
// 2. 找出所有根节点(没有父节点的工序)
var allChildIds = parentChildRelations.Select(r => r.ChildId).ToHashSet();
var rootIds = operationIds.Where(id => !allChildIds.Contains(id)).ToList();
// 3. 初始化根节点层级为0
foreach (var rootId in rootIds)
{
levelMap[rootId] = 0;
}
// 4. 递归计算子节点层级(BFS避免栈溢出)
var queue = new Queue<Guid>(rootIds); // 从根节点开始遍历
while (queue.Count > 0)
{
var parentId = queue.Dequeue();
var parentLevel = levelMap[parentId];
// 找出当前父节点的所有子节点
var childIds = parentChildRelations
.Where(r => r.ParentId == parentId && operationIds.Contains(r.ChildId))
.Select(r => r.ChildId)
.ToList();
foreach (var childId in childIds)
{
if (!levelMap.ContainsKey(childId)) // 避免重复计算
{
levelMap[childId] = parentLevel + 1; // 子节点层级=父节点+1
queue.Enqueue(childId); // 继续计算子节点的子节点
}
}
}
// 5. 处理孤立节点(无任何父子关系,默认层级0)
foreach (var id in operationIds)
{
if (!levelMap.ContainsKey(id))
{
levelMap[id] = 0;
}
}
return levelMap;
}
//public async override Task<PagedResultDto<Dictionary<string, object>>> GetListAsync(OperationGetListInput input)
//{
// var opQry = await operationRepo.GetQueryableAsync();
// var allOperationIds = await opQry
// .Select(o => o.Id)
// .ToListAsync();
// // 获取原始嵌套树形数据
// var nestedTrees = await GetFlatNodesAsync(allOperationIds);
// nestedTrees = nestedTrees.Where(n => n.Level > 0).ToList();
// //var entites = ObjectMapper.Map<List<OperationEntity>, List<Dictionary<string, object>>>(nestedTrees);
// List<Dictionary<string, object>> entites = new List<Dictionary<string, object>>();
// nestedTrees.ForEach(node =>
// {
// var et = Convert(node);
// entites.Add(et);
// });
// return new PagedResultDto<Dictionary<string, object>>(entites.Count, entites);
//}
public async Task<PagedResultDto<OperationGetOutputDto>> GetListTopLevelAsync(OperationGetListInput input)
{
var opQry = await operationRepo.GetQueryableAsync();
var subOpQry = await subOperationRepo.GetQueryableAsync();
List<Guid> subOpIds = await subOpQry
.Select(s => s.SubOperationId)
.Distinct()
.ToListAsync();
opQry = opQry.Where(o => !subOpIds.Contains(o.Id));
opQry = FilterHelper.CreateFilterQuery(input, opQry);
if (!string.IsNullOrWhiteSpace(input.FilterOpt))
{
var filterOpt = JObject.Parse(input.FilterOpt);
var keywords = filterOpt.ContainsKey("keywords") ? filterOpt["keywords"]?.ToString() : null;
if (!string.IsNullOrWhiteSpace(keywords))
{
opQry = opQry.Where(o => o.OperationCode.Contains(keywords) || o.OperationName.Contains(keywords));,在这里实现下面代码 public async Task<PagedResultDto<OperationGetOutputDto>> GetListWithFilterAsync(OperationGetListInput input)
{
// 1. 解析前端传入的FilterOpt JSON字符串
OperationFilterOptions? filter = null;
if (!string.IsNullOrWhiteSpace(input.FilterOpt))
{
try
{
var settings = new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver(),
NullValueHandling = NullValueHandling.Ignore
};
filter = JsonConvert.DeserializeObject<OperationFilterOptions>(input.FilterOpt, settings);
}
catch (Exception)
{
throw new BusinessException("筛选条件格式错误,请传入有效的JSON字符串!");
}
}
var query = await repository.WithDetailsAsync();
Guid? excludedId = null;
if (filter != null && !string.IsNullOrWhiteSpace(filter.Filter_Id))
{
string parentIdStr = filter.Filter_Id.Trim();
const string prefix = "opt_";
// 检查是否以 "opt_" 开头
if (parentIdStr.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
// 提取 "opt_" 后面的部分
string suffix = parentIdStr.Substring(prefix.Length).Trim();
// 按空格分割操作符代码和目标值
string[] parts = suffix.Split(new[] { ' ' }, 2);
// 尝试解析操作符
if (parts.Length > 0 && Enum.TryParse<OperatorType>(parts[0], out var operatorType))
{
// 只有当操作符是 NotEqual 时,才执行排除逻辑
if (operatorType == OperatorType.NotEqual && parts.Length > 1)
{
// 解析真正的ID值
string idValue = parts[1].Trim();
if (Guid.TryParse(idValue, out Guid parsedId))
{
excludedId = parsedId;
}
}
}
}
}
}
但是这个参考代码逻辑
}
}
var entities = await opQry.ToListAsync();
var dtos = ObjectMapper.Map<List<OperationEntity>, List<OperationGetOutputDto>>(entities);
return new PagedResultDto<OperationGetOutputDto>(dtos.Count, dtos);
}
/// <summary>
/// 将对象转换为 Dictionary<string, object>(键为属性名,值为属性值)
/// </summary>
public static Dictionary<string, object> Convert(OperationNodeFlatDto obj)
{
if (obj == null)
throw new ArgumentNullException(nameof(obj), "对象不能为null");
var dict = new Dictionary<string, object>();
// 获取对象的所有公共实例属性
PropertyInfo[] properties = obj.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);
foreach (var prop in properties)
{
// 忽略索引器(如this[])
if (prop.GetIndexParameters().Length > 0)
continue;
// 获取属性值(支持null值)
object value = prop.GetValue(obj);
dict[prop.Name] = value;
}
return dict;
}
public override async Task<OperationGetOutputDto> GetAsync(Guid id)
{
var operation = await operationRepo.GetAsync(id);
// 计算当前工序的层级
var levelMap = await CalculateOperationLevelsAsync(new[] { id });
var dto = ObjectMapper.Map<OperationEntity, OperationGetOutputDto>(operation);
dto.Level = levelMap.TryGetValue(id, out var level) ? level : 0;
return dto;
}
}
}
excludeid为当前子工序的id,想办法返回operation列表不包含excludeid,子工序为excludeid的主工序要是为别人的子工序,那个别人(主工序)不返回,不要在数据库里递归,最好用上面的方法
最新发布