feat(cli): 增强Analyze命令支持自动扫描和Excel输出
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
using System.CommandLine;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Application.Analysis;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Application.Exporting;
|
||||
|
||||
namespace Gpulse.WCT.DataAnalyzer.Commands;
|
||||
|
||||
@@ -8,9 +9,12 @@ namespace Gpulse.WCT.DataAnalyzer.Commands;
|
||||
/// </summary>
|
||||
public class AnalyzeCommand : Command
|
||||
{
|
||||
private static readonly string[] LogExtensions = [".DAT", ".dat", ".LOG", ".log"];
|
||||
|
||||
public AnalyzeCommand(
|
||||
PlossAnalysisService plossAnalysisService,
|
||||
QfodCalibrationService qfodCalibrationService)
|
||||
QfodCalibrationService qfodCalibrationService,
|
||||
AnalyzeExcelExporter excelExporter)
|
||||
: base("analyze", "Analyze Ploss margin or Qfod calibration threshold from log files")
|
||||
{
|
||||
var typeOption = new Option<string>(
|
||||
@@ -36,31 +40,246 @@ public class AnalyzeCommand : Command
|
||||
"Qfod: path to the phone + foreign object charging log file"
|
||||
);
|
||||
|
||||
var outputOption = new Option<string?>(
|
||||
new[] { "--output", "-o" },
|
||||
"Output Excel file path (default: same directory as input with .xlsx extension)"
|
||||
);
|
||||
|
||||
var autoOption = new Option<bool>(
|
||||
"--auto",
|
||||
"Auto scan Datas folder and analyze all files"
|
||||
);
|
||||
|
||||
AddOption(typeOption);
|
||||
AddOption(fileOption);
|
||||
AddOption(pureOption);
|
||||
AddOption(foreignOption);
|
||||
AddOption(outputOption);
|
||||
AddOption(autoOption);
|
||||
|
||||
this.SetHandler(async (type, file, pure, foreign) =>
|
||||
this.SetHandler(async (type, file, pure, foreign, output, auto) =>
|
||||
{
|
||||
type = type.ToLower();
|
||||
|
||||
// 自动扫描模式
|
||||
if (auto || (string.IsNullOrEmpty(file) && string.IsNullOrEmpty(pure) && string.IsNullOrEmpty(foreign)))
|
||||
{
|
||||
await HandleAutoScanAsync(plossAnalysisService, qfodCalibrationService, excelExporter, type);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case "ploss":
|
||||
await HandlePlossAsync(plossAnalysisService, file);
|
||||
await HandlePlossAsync(plossAnalysisService, excelExporter, file, output);
|
||||
break;
|
||||
case "qfod":
|
||||
await HandleQfodAsync(qfodCalibrationService, pure, foreign);
|
||||
await HandleQfodAsync(qfodCalibrationService, excelExporter, pure, foreign, output);
|
||||
break;
|
||||
default:
|
||||
Console.WriteLine($"Unknown type: {type}. Use 'ploss' or 'qfod'.");
|
||||
break;
|
||||
}
|
||||
}, typeOption, fileOption, pureOption, foreignOption);
|
||||
}, typeOption, fileOption, pureOption, foreignOption, outputOption, autoOption);
|
||||
}
|
||||
|
||||
private static async Task HandlePlossAsync(PlossAnalysisService service, string? file)
|
||||
/// <summary>
|
||||
/// 自动扫描 Datas 文件夹并分析
|
||||
/// </summary>
|
||||
private static async Task HandleAutoScanAsync(
|
||||
PlossAnalysisService plossService,
|
||||
QfodCalibrationService qfodService,
|
||||
AnalyzeExcelExporter exporter,
|
||||
string type)
|
||||
{
|
||||
var dataDir = Path.Combine(AppContext.BaseDirectory, "Datas");
|
||||
|
||||
if (!Directory.Exists(dataDir))
|
||||
{
|
||||
Console.WriteLine($"[错误] 未找到 Datas 文件夹: {dataDir}");
|
||||
Console.WriteLine("请创建 Datas 文件夹并放入日志文件,或手动指定文件路径。");
|
||||
return;
|
||||
}
|
||||
|
||||
// 扫描日志文件
|
||||
var allFiles = LogExtensions
|
||||
.SelectMany(ext => Directory.GetFiles(dataDir, $"*{ext}"))
|
||||
.ToList();
|
||||
|
||||
if (allFiles.Count == 0)
|
||||
{
|
||||
Console.WriteLine($"[错误] Datas 文件夹中未找到日志文件");
|
||||
return;
|
||||
}
|
||||
|
||||
// 分类文件
|
||||
var pureFiles = new List<string>();
|
||||
var foreignFiles = new List<string>();
|
||||
|
||||
foreach (var filePath in allFiles)
|
||||
{
|
||||
var foreignType = GetForeignObjectType(Path.GetFileNameWithoutExtension(filePath));
|
||||
if (foreignType == "无")
|
||||
pureFiles.Add(filePath);
|
||||
else
|
||||
foreignFiles.Add(filePath);
|
||||
}
|
||||
|
||||
Console.WriteLine($"\n扫描 Datas 文件夹:");
|
||||
Console.WriteLine($" 总计: {allFiles.Count} 个日志文件");
|
||||
Console.WriteLine($" 纯手机: {pureFiles.Count} 个");
|
||||
Console.WriteLine($" 异物: {foreignFiles.Count} 个");
|
||||
Console.WriteLine();
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case "ploss":
|
||||
Console.WriteLine("=== Ploss 分析 ===");
|
||||
int plossOk = 0, plossFailed = 0;
|
||||
foreach (var filePath in allFiles)
|
||||
{
|
||||
try
|
||||
{
|
||||
await HandlePlossAsync(plossService, exporter, filePath, null);
|
||||
plossOk++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
plossFailed++;
|
||||
Console.WriteLine($"[FAILED] {Path.GetFileName(filePath)}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
Console.WriteLine($"\n[完成] 已分析 {plossOk} 个文件{(plossFailed > 0 ? $",失败 {plossFailed} 个" : "")}");
|
||||
break;
|
||||
|
||||
case "qfod":
|
||||
if (pureFiles.Count == 0)
|
||||
{
|
||||
Console.WriteLine("[错误] 未找到纯手机日志(文件名第7段为\"无\")");
|
||||
return;
|
||||
}
|
||||
if (foreignFiles.Count == 0)
|
||||
{
|
||||
Console.WriteLine("[错误] 未找到异物日志(文件名第7段不为\"无\")");
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine("=== Qfod 标定 ===");
|
||||
|
||||
if (pureFiles.Count == 1 && foreignFiles.Count == 1)
|
||||
{
|
||||
try
|
||||
{
|
||||
await HandleQfodAsync(qfodService, exporter, pureFiles[0], foreignFiles[0], null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[FAILED] {Path.GetFileName(pureFiles[0])}: {ex.Message}");
|
||||
}
|
||||
Console.WriteLine("\n[完成] 已分析 1 个配对");
|
||||
}
|
||||
else
|
||||
{
|
||||
int pairCount = 0, qfodOk = 0, qfodFailed = 0;
|
||||
foreach (var pure in pureFiles)
|
||||
{
|
||||
foreach (var foreign in foreignFiles)
|
||||
{
|
||||
pairCount++;
|
||||
var outputPath = GetOutputPath(pure, null, $"qfod_{pairCount}");
|
||||
try
|
||||
{
|
||||
await HandleQfodAsync(qfodService, exporter, pure, foreign, outputPath);
|
||||
qfodOk++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
qfodFailed++;
|
||||
Console.WriteLine($"[FAILED] {Path.GetFileName(pure)} + {Path.GetFileName(foreign)}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Console.WriteLine($"\n[完成] 已分析 {qfodOk} 个配对{(qfodFailed > 0 ? $",失败 {qfodFailed} 个" : "")}");
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
Console.WriteLine($"Unknown type: {type}. Use 'ploss' or 'qfod'.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从文件名提取异物类型(第7段)
|
||||
/// 格式: 车厂-车型-TX面板-TX硬件-TX软件-RX类型-异物类型-测试目的-年月日测试次数
|
||||
/// </summary>
|
||||
private static string GetForeignObjectType(string fileNameWithoutExt)
|
||||
{
|
||||
var parts = fileNameWithoutExt.Split('-');
|
||||
return parts.Length >= 7 ? parts[6] : "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取输出目录(exe 同目录下的 Output 文件夹)
|
||||
/// </summary>
|
||||
private static string GetOutputDirectory()
|
||||
{
|
||||
var outputDir = Path.Combine(AppContext.BaseDirectory, "Output");
|
||||
if (!Directory.Exists(outputDir))
|
||||
Directory.CreateDirectory(outputDir);
|
||||
return outputDir;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 净化来自文件名的路径段:剔除路径分隔符、遍历段、非法字符,防止 Datas/ 下的文件名
|
||||
/// 经 Path.Combine 写到 Output/ 之外(路径遍历)。不可信输入必须经此处理。
|
||||
/// </summary>
|
||||
private static string SafeSegment(string segment)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(segment))
|
||||
return "Unknown";
|
||||
|
||||
var invalid = Path.GetInvalidFileNameChars()
|
||||
.Concat(['/', '\\', ':', '*', '?', '"', '<', '>', '|']);
|
||||
var cleaned = new string(segment.Where(c => !invalid.Contains(c)).ToArray());
|
||||
return string.IsNullOrWhiteSpace(cleaned) ? "Unknown" : cleaned;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据文件名解析出目录结构:Output/{车厂}/{车型}/{手机名}/
|
||||
/// 文件名格式: 车厂-车型-TX面板-TX硬件-TX软件-RX类型-异物类型-测试目的-年月日测试次数
|
||||
/// </summary>
|
||||
private static string GetOutputPath(string inputFile, string? customOutput = null, string? suffix = null)
|
||||
{
|
||||
if (customOutput != null)
|
||||
return customOutput;
|
||||
|
||||
var fileName = Path.GetFileNameWithoutExtension(inputFile);
|
||||
var parts = fileName.Split('-');
|
||||
|
||||
// 解析车厂、车型、手机名(RX类型),经 SafeSegment 净化防止路径遍历
|
||||
var manufacturer = parts.Length >= 1 ? SafeSegment(parts[0]) : "Unknown";
|
||||
var carModel = parts.Length >= 2 ? SafeSegment(parts[1]) : "Unknown";
|
||||
var phoneName = parts.Length >= 6 ? SafeSegment(parts[5]) : "Unknown";
|
||||
|
||||
// 构建输出目录
|
||||
var outputDir = Path.Combine(GetOutputDirectory(), manufacturer, carModel, phoneName);
|
||||
if (!Directory.Exists(outputDir))
|
||||
Directory.CreateDirectory(outputDir);
|
||||
|
||||
// 构建输出文件名
|
||||
var outputFileName = suffix != null
|
||||
? $"{fileName}_{suffix}.xlsx"
|
||||
: $"{fileName}.xlsx";
|
||||
|
||||
return Path.Combine(outputDir, outputFileName);
|
||||
}
|
||||
|
||||
private static async Task HandlePlossAsync(
|
||||
PlossAnalysisService service,
|
||||
AnalyzeExcelExporter exporter,
|
||||
string? file,
|
||||
string? output)
|
||||
{
|
||||
if (string.IsNullOrEmpty(file))
|
||||
{
|
||||
@@ -68,48 +287,27 @@ public class AnalyzeCommand : Command
|
||||
return;
|
||||
}
|
||||
|
||||
var report = await service.AnalyzeFileAsync(file);
|
||||
var excelReport = await service.AnalyzeForExcelAsync(file);
|
||||
|
||||
if (!report.IsSuccess)
|
||||
if (!excelReport.IsSuccess)
|
||||
{
|
||||
Console.WriteLine($" [{report.FileName}] FAILED: {report.ErrorMessage}");
|
||||
Console.WriteLine($"[FAILED] {excelReport.FileName}: {excelReport.ErrorMessage}");
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine($"\n=== Ploss Analysis: {report.FileName} ===");
|
||||
Console.WriteLine($"Total: {report.TotalCount} (TwoLine: {report.TwoLineCount}, Legacy: {report.LegacyCount}), FOD: {report.FodCount}, Errors: {report.ErrorCount}");
|
||||
var outputPath = GetOutputPath(file, output);
|
||||
|
||||
var normalRatio = Percent(report.NormalAboveCount, report.NormalCount);
|
||||
var fodRatio = Percent(report.FodAboveCount, report.FodCount);
|
||||
Console.WriteLine($"Normal margin>2000: {report.NormalAboveCount}/{report.NormalCount} ({normalRatio}%)");
|
||||
Console.WriteLine($"FOD margin>2000: {report.FodAboveCount}/{report.FodCount} ({fodRatio}%)");
|
||||
Console.WriteLine($"Identity (delta_p = Tx - Rx - pow_loss - ploss): {report.IdentityHeldCount}/{report.IdentityCheckedCount}");
|
||||
|
||||
if (report.FodRows.Count == 0)
|
||||
{
|
||||
Console.WriteLine("No FOD rows (Field12 == 1).");
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine($"\nFOD rows (delta_p back-solve):");
|
||||
foreach (var row in report.FodRows)
|
||||
{
|
||||
var format = row.IsTwoLine ? "two-line" : "legacy";
|
||||
Console.WriteLine($" Line {row.LineNumber,-6} [{format}] pow_loss={row.PowLoss?.ToString() ?? "-"} delta_p={row.DeltaP?.ToString() ?? "-"} ploss={row.Ploss} threshold={row.Threshold} margin={row.Margin}");
|
||||
|
||||
if (row.IsTwoLine && row.DeltaPPrime.HasValue)
|
||||
{
|
||||
Console.WriteLine($" -> delta_p'={row.DeltaPPrime.Value}");
|
||||
Console.WriteLine($" rewrite: {row.RewrittenHeader}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($" (旧单行格式无 header,无法反解 delta_p)");
|
||||
}
|
||||
}
|
||||
await exporter.ExportPlossReportAsync(excelReport, outputPath);
|
||||
Console.WriteLine($"[OK] {outputPath}");
|
||||
Console.WriteLine($"Total: {excelReport.TotalCount}, Dropped: {excelReport.DroppedCount}, Errors: {excelReport.ErrorCount}");
|
||||
}
|
||||
|
||||
private static async Task HandleQfodAsync(QfodCalibrationService service, string? pure, string? foreign)
|
||||
private static async Task HandleQfodAsync(
|
||||
QfodCalibrationService service,
|
||||
AnalyzeExcelExporter exporter,
|
||||
string? pure,
|
||||
string? foreign,
|
||||
string? output)
|
||||
{
|
||||
if (string.IsNullOrEmpty(pure) || string.IsNullOrEmpty(foreign))
|
||||
{
|
||||
@@ -117,28 +315,17 @@ public class AnalyzeCommand : Command
|
||||
return;
|
||||
}
|
||||
|
||||
var report = await service.CalibrateAsync(pure, foreign);
|
||||
var excelReport = await service.CalibrateForExcelAsync(pure, foreign);
|
||||
|
||||
if (!report.IsSuccess)
|
||||
if (!excelReport.IsSuccess)
|
||||
{
|
||||
Console.WriteLine($" FAILED: {report.ErrorMessage}");
|
||||
Console.WriteLine($"[FAILED] {excelReport.ErrorMessage}");
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine("\n=== Qfod Calibration ===");
|
||||
PrintQfodFileSummary("Pure phone", report.Pure!);
|
||||
PrintQfodFileSummary("Foreign object", report.Foreign!);
|
||||
var outputPath = output ?? GetOutputPath(pure, output, "qfod");
|
||||
|
||||
Console.WriteLine($"\nThreshold = (pureMax + foreignMin) / 2 = ({report.Pure!.Max} + {report.Foreign!.Min}) / 2 = {report.Threshold}");
|
||||
Console.WriteLine($"Margin: pureMax - threshold = {report.PureMargin} (负为安全), foreignMin - threshold = {report.ForeignMargin} (正为安全)");
|
||||
await exporter.ExportQfodReportAsync(excelReport, outputPath);
|
||||
Console.WriteLine($"[OK] {outputPath}");
|
||||
}
|
||||
|
||||
private static void PrintQfodFileSummary(string label, QfodFileSummary summary)
|
||||
{
|
||||
Console.WriteLine($" {label}: {summary.FileName}");
|
||||
Console.WriteLine($" count={summary.Count} min={summary.Min} max={summary.Max} avg={summary.Average:F1}");
|
||||
}
|
||||
|
||||
private static string Percent(int part, int total)
|
||||
=> total == 0 ? "0.0" : (part * 100.0 / total).ToString("F1");
|
||||
}
|
||||
|
||||
@@ -43,4 +43,8 @@
|
||||
<EmbeddedResource Include="appsettings.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="analyze.bat" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.CommandLine;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
@@ -10,6 +11,7 @@ using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.Extensions;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Application;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Application.Analysis;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Application.Exporting;
|
||||
|
||||
namespace Gpulse.WCT.DataAnalyzer;
|
||||
|
||||
@@ -50,14 +52,14 @@ public class Program
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// 初始化数据库
|
||||
// 初始化数据库(经 EF 迁移建表/升级,替代 EnsureCreated 以支持既有库升级)
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<WctMinerDbContext>();
|
||||
await dbContext.Database.EnsureCreatedAsync();
|
||||
await DatabaseMigrationInitializer.MigrateOrUpgradeAsync(dbContext);
|
||||
|
||||
var releaseContext = scope.ServiceProvider.GetRequiredService<ReleaseDbContext>();
|
||||
await releaseContext.Database.EnsureCreatedAsync();
|
||||
await DatabaseMigrationInitializer.MigrateOrUpgradeAsync(releaseContext);
|
||||
|
||||
// 种子数据
|
||||
SeedData.Initialize(dbContext);
|
||||
@@ -75,7 +77,8 @@ public class Program
|
||||
rootCommand.AddCommand(new CleanCommand(provider.GetRequiredService<CleanService>()));
|
||||
rootCommand.AddCommand(new AnalyzeCommand(
|
||||
provider.GetRequiredService<PlossAnalysisService>(),
|
||||
provider.GetRequiredService<QfodCalibrationService>()));
|
||||
provider.GetRequiredService<QfodCalibrationService>(),
|
||||
provider.GetRequiredService<AnalyzeExcelExporter>()));
|
||||
|
||||
return await rootCommand.InvokeAsync(args);
|
||||
}
|
||||
|
||||
38
src/Gpulse.WCT.DataAnalyzer/analyze.bat
Normal file
38
src/Gpulse.WCT.DataAnalyzer/analyze.bat
Normal file
@@ -0,0 +1,38 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
|
||||
:: WCT DataAnalyzer 分析工具
|
||||
:: 自动扫描 Datas 文件夹,执行 Ploss 和 Qfod 分析
|
||||
|
||||
set "SCRIPT_DIR=%~dp0"
|
||||
set "EXE_PATH=%SCRIPT_DIR%Gpulse.WCT.DataAnalyzer.exe"
|
||||
|
||||
if not exist "%EXE_PATH%" (
|
||||
echo [错误] 找不到程序: %EXE_PATH%
|
||||
echo 请确保此脚本与 Gpulse.WCT.DataAnalyzer.exe 在同一目录
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo ========================================
|
||||
echo WCT DataAnalyzer 分析工具
|
||||
echo ========================================
|
||||
echo.
|
||||
|
||||
:: 执行 Ploss 分析
|
||||
echo [1/2] 执行 Ploss 分析...
|
||||
"%EXE_PATH%" analyze --type ploss --auto
|
||||
echo.
|
||||
|
||||
:: 执行 Qfod 标定
|
||||
echo [2/2] 执行 Qfod 标定...
|
||||
"%EXE_PATH%" analyze --type qfod --auto
|
||||
echo.
|
||||
|
||||
echo ========================================
|
||||
echo 分析完成
|
||||
echo ========================================
|
||||
echo.
|
||||
echo 输出目录: %SCRIPT_DIR%Output
|
||||
echo.
|
||||
pause
|
||||
Reference in New Issue
Block a user