using System.Collections.Concurrent;
using System.Text.RegularExpressions;
using Dpz.Core.Entity.Base.ExpressTreeQuery;
using Dpz.Core.Public.ViewModel.Request;
using Dpz.Core.Public.ViewModel.RequestEvent;
using Dpz.Core.Public.ViewModel.Response;
namespace Dpz.Core.Service.RepositoryServiceImpl;
public class CodeFileSystemEntryService(
IRepository<CodeFileSystemEntry> repository,
IConfiguration configuration,
ILogger<CodeFileSystemEntryService> logger,
IMediator mediator,
IFusionCache fusionCache
) : AbstractCacheService(fusionCache), ICodeFileSystemEntryService
{
protected override TimeSpan CacheDefaultExpiration => TimeSpan.FromDays(1);
private readonly Lazy<CodeViewOption> _codeViewCfg = new(() =>
{
var codeView = configuration.GetSection("CodeView").Get<CodeViewOption>();
if (
codeView == null
|| string.IsNullOrEmpty(codeView.SourceCodeRoot)
|| !Directory.Exists(codeView.SourceCodeRoot)
)
{
logger.LogError("代码查看配置错误:{Cfg}", JsonSerializer.Serialize(codeView));
throw new BusinessException("appsettings.json 配置错误,源代码目录不存在");
}
return codeView;
});
public async Task<CodeFileSystemEntryResponse?> FindByPathAsync(
IReadOnlyCollection<string>? pathSegments,
CancellationToken cancellationToken = default
)
{
var segments = NormalizeSegments(pathSegments);
var cacheKey = BuildPathKey(segments);
return await GetOrSetCacheAsync<CodeFileSystemEntryResponse?>(
nameof(FindByPathAsync),
async (_, ct) => await FindByPathCoreAsync(segments, ct),
new { Path = cacheKey },
cancellationToken: cancellationToken
);
}
public async Task<CodeFileSystemEntryResponse?> FindByPathWithoutCacheAsync(
IReadOnlyCollection<string>? pathSegments,
CancellationToken cancellationToken = default
)
{
var segments = NormalizeSegments(pathSegments);
return await FindByPathCoreAsync(segments, cancellationToken);
}
public async Task<List<CodeFileSystemEntryResponse>> GetChildrenAsync(
IReadOnlyCollection<string>? parentPathSegments,
CancellationToken cancellationToken = default
)
{
var segments = NormalizeSegments(parentPathSegments);
var cacheKey = BuildPathKey(segments);
return await GetOrSetCacheAsync<List<CodeFileSystemEntryResponse>>(
nameof(GetChildrenAsync),
async (_, ct) =>
{
var filter = Builders<CodeFileSystemEntry>.Filter.Eq(
x => x.ParentPathSegments,
segments
);
var entries = await repository.SearchFor(filter).ToListAsync(ct);
return await MapToResponseListAsync(entries, ct);
},
new { Parent = cacheKey },
cancellationToken: cancellationToken
);
}
public async Task<List<CodeFileSystemEntryResponse>> SearchAsync(
string keyword,
CancellationToken cancellationToken = default
)
{
if (string.IsNullOrWhiteSpace(keyword))
{
return [];
}
return await GetOrSetCacheAsync<List<CodeFileSystemEntryResponse>>(
nameof(SearchAsync),
async (_, ct) =>
{
var filter = Builders<CodeFileSystemEntry>.Filter.Regex(
x => x.Name,
new BsonRegularExpression(keyword, "i")
);
var entries = await repository.SearchFor(filter).ToListAsync(ct);
return await MapToResponseListAsync(entries, ct);
},
new { Keyword = keyword },
cancellationToken: cancellationToken
);
}
public async Task<IPagedList<CodeFileSystemEntryListResponse>> GetPagedListAsync(
CodeFlatRequest request,
CancellationToken cancellationToken = default
)
{
// 生成查询条件
var filter = GenerateConditionFilter.GenerateConditionExpressionTree<
CodeFileSystemEntry,
CodeFlatRequest
>(request);
var query = repository.SearchFor(filter);
// 应用排序
if (request.SortField.HasValue)
{
query = request.SortField.Value switch
{
CodeFileSortField.Size => request.IsDescending
? query.OrderByDescending(x => x.Size)
: query.OrderBy(x => x.Size),
CodeFileSortField.CreatedTime => request.IsDescending
? query.OrderByDescending(x => x.CreatedTime)
: query.OrderBy(x => x.CreatedTime),
CodeFileSortField.LastWriteTime => request.IsDescending
? query.OrderByDescending(x => x.LastWriteTime)
: query.OrderBy(x => x.LastWriteTime),
CodeFileSortField.LastUpdateTime => request.IsDescending
? query.OrderByDescending(x => x.LastUpdateTime)
: query.OrderBy(x => x.LastUpdateTime),
CodeFileSortField.AiAnalyzeTime => request.IsDescending
? query.OrderByDescending(x => x.AiAnalyzeTime)
: query.OrderBy(x => x.AiAnalyzeTime),
_ => query,
};
}
// 执行分页查询
var entityPagedList = await query
.Select(x => new CodeFileSystemEntryListResponse
{
Id = x.Id.ToString(),
PathSegments = x.PathSegments,
Name = x.Name,
ParentPathSegments = x.ParentPathSegments,
IsDirectory = x.IsDirectory,
Extension = x.Extension,
Size = x.Size,
Hash = x.Hash,
CodeFileContentType = x.CodeFileContentType,
CodeLanguage = x.CodeLanguage,
Tags = x.Tags,
CreatedTime = x.CreatedTime,
LastWriteTime = x.LastWriteTime,
LastUpdateTime = x.LastUpdateTime,
Description = x.Description,
AiAnalyzeTime = x.AiAnalyzeTime,
AiAnalyzeHash = x.AiAnalyzeHash,
})
.ToPagedListAsync(request.PageIndex, request.PageSize, cancellationToken);
entityPagedList.ForEach(x =>
x.CodeLanguage = ResolveCodeLanguage(x.IsDirectory, x.Extension, x.Name)
);
return entityPagedList;
}
public async Task<List<string[]>> GetAllDirectoriesAsync(
string[]? pathSegments = null,
CancellationToken cancellationToken = default
)
{
var normalizedPath = NormalizeSegments(pathSegments);
var pathKey = BuildPathKey(normalizedPath);
return await GetOrSetCacheAsync<List<string[]>>(
nameof(GetAllDirectoriesAsync),
async (_, ct) =>
{
var filterBuilder = Builders<CodeFileSystemEntry>.Filter;
var filter = filterBuilder.Eq(x => x.IsDirectory, true);
// 如果指定了路径,则只查询该路径及其所有子目录
if (normalizedPath.Count > 0)
{
// 查询所有 PathSegments 以 normalizedPath 开头的目录
// 先查询所有目录,然后在内存中过滤
var allDirectories = await repository
.SearchFor(filter)
.ToListAsync(cancellationToken: ct);
var filteredDirectories = allDirectories
.Where(x =>
x.PathSegments.Count >= normalizedPath.Count
&& x.PathSegments.Take(normalizedPath.Count)
.SequenceEqual(normalizedPath)
)
.ToList();
return filteredDirectories
.Select(x => x.PathSegments.ToArray())
.OrderBy(x => x.Length)
.ThenBy(x => string.Join("/", x))
.ToList();
}
// 没有指定路径,返回所有目录
var directories = await repository
.SearchFor(filter)
.ToListAsync(cancellationToken: ct);
return directories
.Select(x => x.PathSegments.ToArray())
.OrderBy(x => x.Length)
.ThenBy(x => string.Join("/", x))
.ToList();
},
new { Path = pathKey },
cancellationToken: cancellationToken
);
}
public async Task<bool> SaveDescriptionAsync(
string[]? path,
string name,
string? description,
CancellationToken cancellationToken = default
)
{
if (string.IsNullOrWhiteSpace(name))
{
return false;
}
var segments = NormalizeSegments(path).ToList();
segments.Add(name);
var filter = Builders<CodeFileSystemEntry>.Filter.Eq(x => x.PathSegments, segments);
var entry = await repository.SearchFor(filter).FirstOrDefaultAsync(cancellationToken);
if (entry == null)
{
return false;
}
entry.Description = description;
entry.LastUpdateTime = DateTime.Now;
await repository.UpdateAsync(entry, cancellationToken);
await InvalidateEntryCachesAsync(entry);
await InvalidateSearchCacheAsync(name);
return true;
}
public async Task SaveAiAnalyzeResultAsync(
string[]? path,
string name,
string analyzeResult,
string? analyzedHash = null,
CancellationToken cancellationToken = default
)
{
if (string.IsNullOrWhiteSpace(name))
{
return;
}
var segments = NormalizeSegments(path).ToList();
segments.Add(name);
var filter = Builders<CodeFileSystemEntry>.Filter.Eq(x => x.PathSegments, segments);
var entry = await repository.SearchFor(filter).FirstOrDefaultAsync(cancellationToken);
if (entry == null)
{
return;
}
entry.AiAnalyzeResult = analyzeResult;
entry.AiAnalyzeHash = analyzedHash ?? entry.Hash;
entry.AiAnalyzeTime = DateTime.Now;
entry.LastUpdateTime = DateTime.Now;
await repository.UpdateAsync(entry, cancellationToken);
await InvalidateEntryCachesAsync(entry);
await InvalidateSearchCacheAsync(name);
}
public async Task<bool> ShouldAnalyzeAsync(
string[]? path,
string? name,
CodeContainer? codeContainer,
CancellationToken cancellationToken = default
)
{
var useAiAnalyze = configuration.GetValue("CodeUseAIAnalyze", false);
if (!useAiAnalyze)
{
return false;
}
if (
string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(codeContainer?.CodeContent)
)
{
return false;
}
// 检查代码行数,少于50行不分析
var lineCount = codeContainer
.CodeContent.Split(["\r\n", "\r", "\n"], StringSplitOptions.None)
.Length;
if (lineCount < 50)
{
return false;
}
if (name.Contains(".min"))
{
return false;
}
var language = codeContainer.Language?.ToLowerInvariant();
var isSupportedLanguage = language is "csharp" or "javascript" or "typescript";
if (!isSupportedLanguage)
{
return false;
}
var segments = NormalizeSegments(path).ToList();
var filter = Builders<CodeFileSystemEntry>.Filter.Eq(x => x.PathSegments, segments);
var entry = await repository.SearchFor(filter).FirstOrDefaultAsync(cancellationToken);
if (entry == null)
{
return false;
}
if (string.IsNullOrWhiteSpace(entry.AiAnalyzeResult))
{
return true;
}
if (!string.Equals(entry.AiAnalyzeHash, entry.Hash, StringComparison.OrdinalIgnoreCase))
{
return true;
}
return false;
}
public async Task SyncAsync(CancellationToken cancellationToken = default)
{
var syncStopwatch = Stopwatch.StartNew();
var codeView = _codeViewCfg.Value;
var sourceRoot = codeView.SourceCodeRoot;
logger.LogInformation("开始同步代码,源路径: {SourceRoot}", sourceRoot);
// 构建文件系统映射表
var fileSystemInfos = GetAllFileSystemInfos(sourceRoot, codeView);
var fileSystemMap = new Dictionary<string, FileSystemInfo>(
StringComparer.OrdinalIgnoreCase
);
foreach (var item in fileSystemInfos)
{
var relativeSegments = GetRelativeSegments(sourceRoot, item.FullName);
if (relativeSegments.Count == 0)
{
continue;
}
var key = BuildPathKey(relativeSegments);
fileSystemMap[key] = item;
}
logger.LogInformation("扫描完成,共 {Count} 个文件系统项", fileSystemMap.Count);
// 构建数据库映射表
var dbEntries = await repository.SearchFor(x => true).ToListAsync(cancellationToken);
var dbMap = dbEntries.ToDictionary(
x => BuildPathKey(x.PathSegments),
StringComparer.OrdinalIgnoreCase
);
var insertList = new ConcurrentBag<CodeFileSystemEntry>();
var updateList = new ConcurrentBag<CodeFileSystemEntry>();
var deleteIds = new List<ObjectId>();
// 并行处理文件:比对文件系统与数据库,找出需要新增或更新的项
var maxDegreeOfParallelism = Math.Max(1, Environment.ProcessorCount / 2);
await Parallel.ForEachAsync(
fileSystemMap,
new ParallelOptions
{
MaxDegreeOfParallelism = maxDegreeOfParallelism,
CancellationToken = cancellationToken,
},
async (pair, ct) =>
{
if (!dbMap.TryGetValue(pair.Key, out var exist))
{
var entry = await BuildEntryAsync(codeView, pair.Value, ct);
if (entry != null)
{
insertList.Add(entry);
}
return;
}
var updated = await BuildUpdatedEntryAsync(codeView, exist, pair.Value, ct);
if (updated != null)
{
updateList.Add(updated);
}
}
);
// 找出需要删除的项(DB 中存在但文件系统中已不存在)
foreach (var entry in dbEntries)
{
var key = BuildPathKey(entry.PathSegments);
if (!fileSystemMap.ContainsKey(key))
{
deleteIds.Add(entry.Id);
}
}
var insertListFinal = insertList.ToList();
var updateListFinal = updateList.ToList();
// 批量执行数据库操作
if (insertListFinal.Count > 0)
{
await repository.InsertAsync(insertListFinal, cancellationToken);
logger.LogInformation("已新增 {Count} 条记录", insertListFinal.Count);
}
if (updateListFinal.Count > 0)
{
await repository.UpdateAsync(updateListFinal, cancellationToken);
logger.LogInformation("已更新 {Count} 条记录", updateListFinal.Count);
}
if (deleteIds.Count > 0)
{
var filter = Builders<CodeFileSystemEntry>.Filter.In(x => x.Id, deleteIds);
await repository.DeleteAsync(filter, cancellationToken);
logger.LogInformation("已删除 {Count} 条记录", deleteIds.Count);
}
syncStopwatch.Stop();
logger.LogInformation(
"代码同步完成,新增:{InsertCount} 条,更新:{UpdateCount} 条,删除:{DeleteCount} 条,耗时:{ElapsedMs}ms",
insertListFinal.Count,
updateListFinal.Count,
deleteIds.Count,
syncStopwatch.ElapsedMilliseconds
);
if (insertListFinal.Count > 0)
{
await mediator.Send(
new CodeCompatibleRequest
{
NewEntries = insertListFinal
.Select(x => new CodeCompatibleEntry
{
ParentPathSegments = x.ParentPathSegments,
Name = x.Name,
})
.ToList(),
},
cancellationToken
);
}
await RemoveByMethodAsync(nameof(FindByPathAsync), cancellationToken);
await RemoveByMethodAsync(nameof(GetChildrenAsync), cancellationToken);
await RemoveByMethodAsync(nameof(SearchAsync), cancellationToken);
}
private async Task InvalidateEntryCachesAsync(CodeFileSystemEntry entry)
{
var entryKey = BuildPathKey(entry.PathSegments);
await RemoveCacheAsync(nameof(FindByPathAsync), new { Path = entryKey });
var parentKey = BuildPathKey(entry.ParentPathSegments);
await RemoveCacheAsync(nameof(GetChildrenAsync), new { Parent = parentKey });
}
private async Task<CodeFileSystemEntryResponse?> FindByPathCoreAsync(
List<string> segments,
CancellationToken cancellationToken
)
{
// 先尝试精确匹配(区分大小写)
var filter = Builders<CodeFileSystemEntry>.Filter.Eq(x => x.PathSegments, segments);
var entry = await repository.SearchFor(filter).FirstOrDefaultAsync(cancellationToken);
if (entry == null && segments.Count > 0)
{
// 如果精确匹配失败,则构建不区分大小写的正则过滤器
var regexes = segments
.Select(x => new BsonRegularExpression($"^{Regex.Escape(x)}$", "i"))
.ToArray();
var caseInsensitiveFilter = new BsonDocumentFilterDefinition<CodeFileSystemEntry>(
new BsonDocument
{
{
nameof(CodeFileSystemEntry.PathSegments),
new BsonDocument
{
{ "$size", segments.Count },
{ "$all", new BsonArray(regexes) },
}
},
}
);
#if DEBUG
var serializerRegistry = BsonSerializer.SerializerRegistry;
var documentSerializer = serializerRegistry.GetSerializer<CodeFileSystemEntry>();
var serializerArgs = new RenderArgs<CodeFileSystemEntry>(
documentSerializer,
serializerRegistry
);
var bsonDocumentQuery = caseInsensitiveFilter.Render(serializerArgs);
logger.LogInformation(
"构建查询条件: {BsonDocumentQuery}",
bsonDocumentQuery.ToString()
);
#endif
entry = await repository
.SearchFor(caseInsensitiveFilter)
.FirstOrDefaultAsync(cancellationToken);
}
if (entry == null)
{
return null;
}
var readmeContent = entry.IsDirectory
? await ResolveReadmeContentAsync(entry.PathSegments, cancellationToken)
: null;
return MapToResponse(entry, readmeContent);
}
private async Task InvalidateSearchCacheAsync(string name)
{
if (string.IsNullOrWhiteSpace(name))
{
return;
}
await RemoveCacheAsync(nameof(SearchAsync), new { Keyword = name });
}
private static List<string> NormalizeSegments(IReadOnlyCollection<string>? segments)
{
return segments?.Where(x => !string.IsNullOrWhiteSpace(x)).ToList() ?? [];
}
private CodeFileSystemEntryResponse MapToResponse(
CodeFileSystemEntry entry,
string? readmeContent
)
{
return new CodeFileSystemEntryResponse
{
Id = entry.Id.ToString(),
PathSegments = entry.PathSegments.ToList(),
Name = entry.Name,
ParentPathSegments = entry.ParentPathSegments.ToList(),
IsDirectory = entry.IsDirectory,
Extension = entry.Extension,
Size = entry.Size,
Hash = entry.Hash,
CodeFileContentType = entry.CodeFileContentType,
FileContent = entry.FileContent,
CodeLanguage = ResolveCodeLanguage(entry.IsDirectory, entry.Extension, entry.Name),
Tags = entry.Tags.ToList(),
CreatedTime = entry.CreatedTime,
LastWriteTime = entry.LastWriteTime,
LastUpdateTime = entry.LastUpdateTime,
Description = entry.Description,
AiAnalyzeResult = entry.AiAnalyzeResult,
AiAnalyzeTime = entry.AiAnalyzeTime,
AiAnalyzeHash = entry.AiAnalyzeHash,
ReadmeContent = readmeContent,
};
}
private async Task<List<CodeFileSystemEntryResponse>> MapToResponseListAsync(
List<CodeFileSystemEntry> entries,
CancellationToken cancellationToken
)
{
if (entries.Count == 0)
{
return [];
}
var directories = entries.Where(x => x.IsDirectory).ToList();
var readmeMap = await ResolveReadmeContentMapAsync(directories, cancellationToken);
return entries
.Select(entry =>
{
string? readmeContent = null;
if (
entry.IsDirectory
&& readmeMap.TryGetValue(BuildPathKey(entry.PathSegments), out var content)
)
{
readmeContent = content;
}
return MapToResponse(entry, readmeContent);
})
.ToList();
}
private string? ResolveCodeLanguage(bool isDirectory, string? extension, string? name)
{
if (isDirectory)
{
return null;
}
var ext = string.IsNullOrWhiteSpace(extension) ? name : extension;
if (string.IsNullOrWhiteSpace(ext))
{
return null;
}
var normalized = ext.ToLower();
return
_codeViewCfg.Value.ExtensionToLanguage?.TryGetValue(normalized, out var language)
== true
? language
: null;
}
private async Task<Dictionary<string, string?>> ResolveReadmeContentMapAsync(
List<CodeFileSystemEntry> directories,
CancellationToken cancellationToken
)
{
if (directories.Count == 0)
{
return new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
}
var parentSegmentsList = directories.Select(x => x.PathSegments).ToList();
var filter = Builders<CodeFileSystemEntry>.Filter.And(
Builders<CodeFileSystemEntry>.Filter.In(x => x.ParentPathSegments, parentSegmentsList),
Builders<CodeFileSystemEntry>.Filter.Regex(
x => x.Name,
new BsonRegularExpression("^readme\\.md$", "i")
)
);
var readmes = await repository.SearchFor(filter).ToListAsync(cancellationToken);
return readmes.ToDictionary(
x => BuildPathKey(x.ParentPathSegments),
x => x.FileContent,
StringComparer.OrdinalIgnoreCase
);
}
private async Task<string?> ResolveReadmeContentAsync(
List<string> parentPathSegments,
CancellationToken cancellationToken
)
{
var filter = Builders<CodeFileSystemEntry>.Filter.And(
Builders<CodeFileSystemEntry>.Filter.Eq(x => x.ParentPathSegments, parentPathSegments),
Builders<CodeFileSystemEntry>.Filter.Regex(
x => x.Name,
new BsonRegularExpression("^readme\\.md$", "i")
)
);
var readme = await repository.SearchFor(filter).FirstOrDefaultAsync(cancellationToken);
return readme?.FileContent;
}
private static string BuildPathKey(IEnumerable<string> segments)
{
return string.Join("/", segments);
}
/// <summary>
/// 获取相对路径
/// </summary>
/// <param name="root"></param>
/// <param name="fullPath"></param>
/// <returns></returns>
private static List<string> GetRelativeSegments(string root, string fullPath)
{
var relativePath = Path.GetRelativePath(root, fullPath);
if (relativePath == ".")
{
return [];
}
return relativePath
.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
.Where(x => !string.IsNullOrWhiteSpace(x))
.ToList();
}
/// <summary>
/// 文件/目录是否过滤
/// </summary>
/// <param name="codeView"></param>
/// <param name="name"></param>
/// <returns></returns>
private static bool IsFiltered(CodeViewOption codeView, string name)
{
return codeView.Filters?.Any(x => x.WildCardMatch(name)) == true;
}
/// <summary>
/// 获取文件扩展名
/// </summary>
/// <param name="fileInfo"></param>
/// <returns></returns>
private static string GetFileExtension(FileInfo fileInfo)
{
var extension = fileInfo.Extension;
return (string.IsNullOrEmpty(extension) ? fileInfo.Name : extension).ToLower();
}
/// <summary>
/// 计算文件哈希值
/// </summary>
private async Task<string> ComputeHashAsync(
FileInfo fileInfo,
CancellationToken cancellationToken
)
{
try
{
await using var stream = fileInfo.OpenRead();
var bytes = await MD5.HashDataAsync(stream, cancellationToken);
var builder = new StringBuilder(bytes.Length * 2);
foreach (var item in bytes)
{
builder.Append(item.ToString("x2"));
}
return builder.ToString();
}
catch (Exception ex)
{
logger.LogWarning(ex, "读取文件哈希失败: {FilePath}", fileInfo.FullName);
return string.Empty;
}
}
/// <summary>
/// 读取文本文件内容
/// </summary>
private async Task<string?> ReadTextContentAsync(
FileInfo fileInfo,
CancellationToken cancellationToken
)
{
try
{
using var reader = fileInfo.OpenText();
return await reader.ReadToEndAsync(cancellationToken);
}
catch (Exception ex)
{
logger.LogWarning(ex, "读取文件内容失败: {FilePath}", fileInfo.FullName);
return null;
}
}
/// <summary>
/// 生成文件系统项
/// </summary>
/// <param name="codeView"></param>
/// <param name="info"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
private async Task<CodeFileSystemEntry?> BuildEntryAsync(
CodeViewOption codeView,
FileSystemInfo info,
CancellationToken cancellationToken
)
{
if (IsFiltered(codeView, info.Name))
{
return null;
}
var relativeSegments = GetRelativeSegments(codeView.SourceCodeRoot, info.FullName);
if (relativeSegments.Count == 0)
{
return null;
}
var parentSegments = relativeSegments.Take(relativeSegments.Count - 1).ToList();
if (info is DirectoryInfo directoryInfo)
{
return new CodeFileSystemEntry
{
PathSegments = relativeSegments,
ParentPathSegments = parentSegments,
Name = directoryInfo.Name,
IsDirectory = true,
Extension = null,
Size = null,
Hash = null,
CodeFileContentType = CodeFileContentType.Unknown,
FileContent = null,
CreatedTime = directoryInfo.CreationTime,
LastWriteTime = directoryInfo.LastWriteTime,
LastUpdateTime = DateTime.Now,
};
}
if (info is FileInfo fileInfo)
{
var extension = GetFileExtension(fileInfo);
var hash = await ComputeHashAsync(fileInfo, cancellationToken);
var shouldPreview = codeView.ExtensionToLanguage?.ContainsKey(extension) == true;
var content = shouldPreview
? await ReadTextContentAsync(fileInfo, cancellationToken)
: null;
return new CodeFileSystemEntry
{
PathSegments = relativeSegments,
ParentPathSegments = parentSegments,
Name = fileInfo.Name,
IsDirectory = false,
Extension = extension,
Size = fileInfo.Length,
Hash = hash,
CodeFileContentType = shouldPreview
? CodeFileContentType.Text
: CodeFileContentType.Unknown,
FileContent = content,
CreatedTime = fileInfo.CreationTime,
LastWriteTime = fileInfo.LastWriteTime,
LastUpdateTime = DateTime.Now,
};
}
return null;
}
/// <summary>
/// 生成需要更新的文件系统项
/// </summary>
/// <param name="codeView"></param>
/// <param name="exist"></param>
/// <param name="info"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
private async Task<CodeFileSystemEntry?> BuildUpdatedEntryAsync(
CodeViewOption codeView,
CodeFileSystemEntry exist,
FileSystemInfo info,
CancellationToken cancellationToken
)
{
if (IsFiltered(codeView, info.Name))
{
return null;
}
if (info is DirectoryInfo directoryInfo)
{
if (
exist.IsDirectory
&& DateTimeAreEqual(exist.LastWriteTime, directoryInfo.LastWriteTime)
&& exist.Name == directoryInfo.Name
)
{
return null;
}
exist.IsDirectory = true;
exist.Name = directoryInfo.Name;
exist.Extension = null;
exist.Size = null;
exist.Hash = null;
exist.CodeFileContentType = CodeFileContentType.Unknown;
exist.FileContent = null;
exist.LastWriteTime = directoryInfo.LastWriteTime;
exist.LastUpdateTime = DateTime.Now;
return exist;
}
if (info is FileInfo fileInfo)
{
var extension = GetFileExtension(fileInfo);
var hash = await ComputeHashAsync(fileInfo, cancellationToken);
var shouldPreview = codeView.ExtensionToLanguage?.ContainsKey(extension) == true;
var needUpdate =
exist.IsDirectory
|| !string.Equals(exist.Hash, hash, StringComparison.OrdinalIgnoreCase)
|| exist.Size != fileInfo.Length
|| !DateTimeAreEqual(exist.LastWriteTime, fileInfo.LastWriteTime)
|| !string.Equals(exist.Extension, extension, StringComparison.OrdinalIgnoreCase);
if (!needUpdate)
{
return null;
}
exist.IsDirectory = false;
exist.Extension = extension;
exist.Size = fileInfo.Length;
exist.Hash = hash;
exist.LastWriteTime = fileInfo.LastWriteTime;
exist.LastUpdateTime = DateTime.Now;
exist.CodeFileContentType = shouldPreview
? CodeFileContentType.Text
: CodeFileContentType.Unknown;
exist.FileContent = shouldPreview
? await ReadTextContentAsync(fileInfo, cancellationToken)
: null;
// 如果文件已有AI分析结果,但修改后行数少于50行,清空分析结果
if (
!string.IsNullOrWhiteSpace(exist.AiAnalyzeResult)
&& !string.IsNullOrWhiteSpace(exist.FileContent)
)
{
var lineCount = exist
.FileContent.Split(["\r\n", "\r", "\n"], StringSplitOptions.None)
.Length;
if (lineCount < 50)
{
exist.AiAnalyzeResult = null;
exist.AiAnalyzeHash = null;
exist.AiAnalyzeTime = null;
}
}
return exist;
}
return null;
}
private static bool DateTimeAreEqual(DateTime dt1, DateTime dt2)
{
// 秒级精度,忽略小数部分
var diffSeconds = (int)(dt1 - dt2).TotalSeconds;
return diffSeconds == 0;
}
/// <summary>
/// 递归获取目录下所有文件和子目录(自动过滤)
/// </summary>
private List<FileSystemInfo> GetAllFileSystemInfos(string path, CodeViewOption codeView)
{
if (string.IsNullOrEmpty(path))
{
return [];
}
var dir = new DirectoryInfo(path);
if (!dir.Exists)
{
return [];
}
var allFiles = new ConcurrentBag<FileSystemInfo>();
try
{
var fileSystemInfos = dir.GetFileSystemInfos();
var subDirectories = new List<DirectoryInfo>();
var currentFiles = new List<FileSystemInfo>();
// 先分类:子目录 vs 文件
foreach (var item in fileSystemInfos)
{
if (IsFiltered(codeView, item.Name))
{
continue;
}
if (item is DirectoryInfo subDir)
{
currentFiles.Add(subDir);
subDirectories.Add(subDir);
}
else
{
currentFiles.Add(item);
}
}
// 当前层级的文件/目录加入结果
foreach (var file in currentFiles)
{
allFiles.Add(file);
}
// 并行扫描子目录(控制并行度避免磁盘抖动)
if (subDirectories.Count > 0)
{
Parallel.ForEach(
subDirectories,
new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount / 2 },
subDir =>
{
var subFiles = GetAllFileSystemInfos(subDir.FullName, codeView);
foreach (var subFile in subFiles)
{
allFiles.Add(subFile);
}
}
);
}
}
catch (UnauthorizedAccessException ex)
{
logger.LogWarning(ex, "无权限访问目录: {Directory}", dir.FullName);
}
return allFiles.ToList();
}
}
⚠⚠ 以下内容为AI分析的结果,请根据实际情况进行判断。
这是一个用于管理代码文件系统条目的服务类,主要功能是同步、查询和管理代码仓库中的文件和目录信息。以下是详细解释:
类的基本结构
public class CodeFileSystemEntryService : AbstractCacheService, ICodeFileSystemEntryService
这个服务类继承自抽象缓存服务,实现了代码文件系统条目服务接口,使用依赖注入获取以下服务:
IRepository<CodeFileSystemEntry>- 数据仓储IConfiguration- 配置服务ILogger- 日志服务IMediator- 中介者模式IFusionCache- 缓存服务
主要功能模块
1. 配置管理
private readonly Lazy<CodeViewOption> _codeViewCfg
延迟加载代码查看配置,包括源代码根目录验证。
2. 查询功能
FindByPathAsync- 根据路径查找文件/目录(带缓存)FindByPathWithoutCacheAsync- 无缓存查找GetChildrenAsync- 获取子项列表SearchAsync- 关键词搜索GetPagedListAsync- 分页查询,支持多字段排序GetAllDirectoriesAsync- 获取所有目录
3. 数据维护功能
SaveDescriptionAsync- 保存描述信息SaveAiAnalyzeResultAsync- 保存AI分析结果ShouldAnalyzeAsync- 判断是否需要AI分析
4. 同步功能(核心)
SyncAsync 方法是最重要的功能,执行以下操作:
- 文件系统扫描:递归扫描源代码目录,构建文件系统映射表
- 数据库对比:获取数据库中现有记录,构建映射表
- 差异分析:并行比较文件系统与数据库,找出需要:
- 新增的项目
- 更新的项目
- 删除的项目
- 批量操作:执行批量插入、更新、删除操作
- 缓存清理:清理相关缓存
- 事件通知:发送代码兼容性检查请求
核心算法特点
1. 并行处理
await Parallel.ForEachAsync(fileSystemMap, new ParallelOptions
{
MaxDegreeOfParallelism = Math.Max(1, Environment.ProcessorCount / 2),
CancellationToken = cancellationToken,
}, async (pair, ct) => { ... });
使用并行处理提高大量文件的处理效率。
2. 智能缓存
- 使用FusionCache进行多级缓存
- 支持缓存失效策略
- 缓存时间为1天
3. 文件过滤
根据配置自动过滤不需要的文件和目录。
4. 文件内容处理
- 自动识别文本文件并读取内容
- 计算文件MD5哈希值用于变更检测
- 支持多种编程语言识别
5. AI分析集成
- 判断文件是否需要AI分析(支持C#、JavaScript、TypeScript)
- 过滤小文件(少于50行)和压缩文件
- 基于文件哈希值判断是否需要重新分析
性能优化
- 并行扫描:文件系统扫描和数据库比较都使用并行处理
- 批量操作:数据库操作采用批量模式
- 缓存策略:查询结果缓存,减少重复计算
- 内存优化:使用ConcurrentBag避免锁竞争
错误处理
- 文件访问权限异常处理
- 文件读取失败的降级处理
- 配置验证和错误提示
这个服务是一个完整的代码仓库管理系统的核心组件,提供了从文件系统同步到智能分析的完整功能链路。
评论加载中...