using System.Diagnostics; using System.Text; using EdiApiGateway.Models; namespace EdiApiGateway.Services; public class EdiCommunicationService { private readonly IConfiguration _configuration; private readonly ILogger _logger; public EdiCommunicationService(IConfiguration configuration, ILogger logger) { _configuration = configuration; _logger = logger; } /// /// AnyTran変換バッチを実行してCSVファイルを生成 /// /// パートナー設定 /// 変換元ファイルパス /// 変換後CSVファイルパス、失敗時はnull private string? ExecuteAnyTranConversion(EdiPartnerConfig config, string inputFilePath) { if (!config.UseAnyTranConversion || string.IsNullOrEmpty(config.AnyTranBatchPath)) { return null; } try { _logger.LogInformation($"[AnyTran] Starting conversion: batch={config.AnyTranBatchPath}, input={inputFilePath}"); // バッチファイル存在チェック if (!File.Exists(config.AnyTranBatchPath)) { _logger.LogError($"[AnyTran] Batch file not found: {config.AnyTranBatchPath}"); return null; } // 出力ファイルパス string outputFilePath = Path.Combine(config.ReceiveBasePath, config.AnyTranOutputFileName); // 既存の出力ファイルがあれば削除 if (File.Exists(outputFilePath)) { File.Delete(outputFilePath); _logger.LogDebug($"[AnyTran] Deleted existing output file: {outputFilePath}"); } // バッチ実行 var processInfo = new ProcessStartInfo { FileName = "cmd.exe", Arguments = $"/c \"{config.AnyTranBatchPath}\"", UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true, WorkingDirectory = Path.GetDirectoryName(config.AnyTranBatchPath) ?? "" }; using var process = new Process { StartInfo = processInfo }; process.Start(); string stdout = process.StandardOutput.ReadToEnd(); string stderr = process.StandardError.ReadToEnd(); // タイムアウト30秒 bool completed = process.WaitForExit(30000); if (!completed) { _logger.LogError("[AnyTran] Batch execution timed out (30 seconds)"); try { process.Kill(); } catch { } return null; } _logger.LogInformation($"[AnyTran] Batch completed: exitCode={process.ExitCode}"); if (!string.IsNullOrEmpty(stdout)) { _logger.LogDebug($"[AnyTran] stdout: {stdout}"); } if (!string.IsNullOrEmpty(stderr)) { _logger.LogWarning($"[AnyTran] stderr: {stderr}"); } // 変換後ファイル存在チェック if (!File.Exists(outputFilePath)) { _logger.LogError($"[AnyTran] Output file not created: {outputFilePath}"); return null; } var fileInfo = new FileInfo(outputFilePath); _logger.LogInformation($"[AnyTran] Conversion successful: {outputFilePath} ({fileInfo.Length} bytes)"); return outputFilePath; } catch (Exception ex) { _logger.LogError(ex, "[AnyTran] Conversion failed"); return null; } } /// /// パートナー設定取得 /// private EdiPartnerConfig GetPartnerConfig(string partner) { var config = _configuration.GetSection($"EdiPartners:{partner}").Get(); if (config == null || string.IsNullOrEmpty(config.Uid)) { throw new InvalidOperationException($"Partner '{partner}' configuration not found"); } return config; } /// /// EDIファイル受信(同期実行) /// public ReceiveResponse ReceiveFile(ReceiveRequest request) { try { var config = GetPartnerConfig(request.Partner); // デバッグモード確認 bool useFixedFile = _configuration.GetValue("DebugMode:UseFixedFile", false); if (useFixedFile) { _logger.LogWarning($"[{request.Partner}] ⚠️ DEBUG MODE: Using fixed file instead of ACMS communication"); // 固定ファイルのパス構築 string fixedFileBasePath = _configuration.GetValue("DebugMode:FixedFileBasePath") ?? "D:\\work\\gentos\\pr_cc\\csharp-edi-api"; string fixedFilePath = Path.Combine(fixedFileBasePath, config.ReceiveFileName); _logger.LogInformation($"[{request.Partner}] Reading fixed file: {fixedFilePath}"); // 固定ファイル存在チェック if (!File.Exists(fixedFilePath)) { _logger.LogError($"[{request.Partner}] Fixed file not found: {fixedFilePath}"); return new ReceiveResponse { Success = false, ReturnCode = -1, Error = $"デバッグ用固定ファイルが見つかりません: {fixedFilePath}" }; } // 固定ファイル読み込み try { // AnyTran変換が必要な場合(ヨドバシ等) string fileToRead = fixedFilePath; if (config.UseAnyTranConversion) { _logger.LogInformation($"[{request.Partner}] AnyTran conversion enabled, executing conversion..."); string? convertedPath = ExecuteAnyTranConversion(config, fixedFilePath); if (convertedPath != null) { fileToRead = convertedPath; _logger.LogInformation($"[{request.Partner}] Using converted file: {fileToRead}"); } else { _logger.LogError($"[{request.Partner}] AnyTran conversion failed, returning error"); return new ReceiveResponse { Success = false, ReturnCode = -1, Error = "AnyTran変換に失敗しました" }; } } // SHIFT-JISのバイナリデータとして読み込み(文字列変換しない) byte[] rawBytes = File.ReadAllBytes(fileToRead); string rawBase64 = Convert.ToBase64String(rawBytes); _logger.LogInformation($"[{request.Partner}] File read successfully: {rawBytes.Length} bytes (Base64: {rawBase64.Length} chars)"); // デバッグ用: 先頭20バイトのHEX表示 string firstBytesHex = BitConverter.ToString(rawBytes.Take(20).ToArray()).Replace("-", ""); _logger.LogDebug($"[{request.Partner}] First 20 bytes (HEX): {firstBytesHex}"); return new ReceiveResponse { Success = true, FilePath = fileToRead, FileContentRaw = rawBase64, // Base64エンコードされたバイト配列(変換後はCSV) ReceivedAt = DateTime.Now, ReturnCode = 0 // 成功 }; } catch (Exception ex) { _logger.LogError(ex, $"[{request.Partner}] Failed to read fixed file: {fixedFilePath}"); return new ReceiveResponse { Success = false, ReturnCode = -1, Error = $"固定ファイル読み込みエラー: {ex.Message}" }; } } // 通常モード: ACMS通信処理 _logger.LogInformation($"[{request.Partner}] Normal mode: Using ACMS communication"); // 保存ディレクトリ作成 Directory.CreateDirectory(config.ReceiveBasePath); // ファイル名は設定ファイルから取得(完全固定) string fileName = config.ReceiveFileName; string fullPath = Path.Combine(config.ReceiveBasePath, fileName); _logger.LogInformation($"[{request.Partner}] Starting ACMSCallRecv: uid={config.Uid}, fid={config.ReceiveFid}, fileName={fileName}, path={fullPath}"); // ACMSCallRecv実行(接続→受信→切断を一括実行) var msgBuffer = AcmsApiWrapper.CreateMessageBuffer(); int ret = AcmsApiWrapper.ACMSCallRecv( config.Uid, config.ReceiveFid, fullPath, request.Cycle, request.Denbun, msgBuffer); // ユーザーフレンドリーなメッセージに変換 string userMessage = AcmsErrorMessageMapper.GetUserFriendlyMessage(ret, msgBuffer); // ログには元のrawメッセージも記録(診断用) string rawMessage = AcmsApiWrapper.DecodeMessage(msgBuffer); _logger.LogInformation($"[{request.Partner}] ACMSCallRecv returned: ret={ret}, raw={rawMessage}, user={userMessage}"); // 戻り値チェック if (ret == 3) { // ファイル無し(データなし、正常な状態) _logger.LogInformation($"[{request.Partner}] No file available on server"); return new ReceiveResponse { Success = true, // データなしは正常状態として扱う ReturnCode = ret, ReceivedAt = DateTime.Now, Error = "サーバーに受信データがありません" }; } else if (ret != 0) { // エラー return new ReceiveResponse { Success = false, ReturnCode = ret, Error = userMessage }; } // 正常終了 - ファイル読み込み try { // AnyTran変換が必要な場合(ヨドバシ等) string fileToRead = fullPath; if (config.UseAnyTranConversion) { _logger.LogInformation($"[{request.Partner}] AnyTran conversion enabled, executing conversion..."); string? convertedPath = ExecuteAnyTranConversion(config, fullPath); if (convertedPath != null) { fileToRead = convertedPath; _logger.LogInformation($"[{request.Partner}] Using converted file: {fileToRead}"); } else { _logger.LogError($"[{request.Partner}] AnyTran conversion failed, returning error"); return new ReceiveResponse { Success = false, ReturnCode = -1, Error = "AnyTran変換に失敗しました" }; } } // バイナリデータとして読み込み(文字列変換しない) byte[] rawBytes = File.ReadAllBytes(fileToRead); string rawBase64 = Convert.ToBase64String(rawBytes); _logger.LogInformation($"[{request.Partner}] File read successfully: {rawBytes.Length} bytes (Base64: {rawBase64.Length} chars)"); // デバッグ用: 先頭20バイトのHEX表示 string firstBytesHex = BitConverter.ToString(rawBytes.Take(20).ToArray()).Replace("-", ""); _logger.LogDebug($"[{request.Partner}] First 20 bytes (HEX): {firstBytesHex}"); var response = new ReceiveResponse { Success = true, FilePath = fileToRead, FileContentRaw = rawBase64, // Base64エンコードされたバイト配列(変換後はCSV) ReceivedAt = DateTime.Now, ReturnCode = ret }; _logger.LogInformation($"[{request.Partner}] ReceiveResponse created successfully, preparing to return JSON"); return response; } catch (FileNotFoundException) { _logger.LogWarning($"[{request.Partner}] File was not created by ACMSCallRecv: {fullPath}"); return new ReceiveResponse { Success = false, ReturnCode = -1, Error = $"ファイルが見つかりません: {fullPath}" }; } catch (DecoderFallbackException ex) { _logger.LogError(ex, $"[{request.Partner}] Failed to decode file as Shift_JIS: {fullPath}"); return new ReceiveResponse { Success = false, ReturnCode = -1, Error = "文字コード変換エラー(Shift_JIS)" }; } } catch (Exception ex) { _logger.LogError(ex, $"[{request.Partner}] ReceiveFile error: {ex.Message}"); return new ReceiveResponse { Success = false, ReturnCode = -1, Error = ex.Message }; } } /// /// EDIファイル送信(同期実行) /// public SendResponse SendFile(SendRequest request) { try { var config = GetPartnerConfig(request.Partner); // 送信ディレクトリ作成 Directory.CreateDirectory(config.SendBasePath); string filePath; // ファイルパス指定 or コンテンツから保存 if (!string.IsNullOrEmpty(request.FilePath)) { // パストラバーサル脆弱性対策: パスの正規化と検証 string normalizedPath = Path.GetFullPath(request.FilePath); string basePath = Path.GetFullPath(config.SendBasePath); if (!normalizedPath.StartsWith(basePath, StringComparison.OrdinalIgnoreCase)) { _logger.LogWarning($"[{request.Partner}] Path traversal attempt detected: {request.FilePath}"); return new SendResponse { Success = false, ReturnCode = -1, Error = "Invalid file path" }; } filePath = normalizedPath; } else if (!string.IsNullOrEmpty(request.FileContent)) { // ファイル名は設定ファイルから取得(完全固定) string fileName = config.SendFileName; filePath = Path.Combine(config.SendBasePath, fileName); try { // UTF-8 → SHIFT-JIS変換 File.WriteAllText(filePath, request.FileContent, Encoding.GetEncoding("Shift_JIS")); } catch (IOException ex) { _logger.LogError(ex, $"[{request.Partner}] Failed to write file: {filePath}"); return new SendResponse { Success = false, ReturnCode = -1, Error = $"ファイル書き込みエラー: {ex.Message}" }; } catch (EncoderFallbackException ex) { _logger.LogError(ex, $"[{request.Partner}] Failed to encode content as Shift_JIS"); return new SendResponse { Success = false, ReturnCode = -1, Error = "文字コード変換エラー(Shift_JIS)" }; } } else { return new SendResponse { Success = false, ReturnCode = -1, Error = "FilePath or FileContent is required" }; } if (!File.Exists(filePath)) { return new SendResponse { Success = false, ReturnCode = -1, Error = $"File not found: {filePath}" }; } // デバッグモード確認 bool useFixedFile = _configuration.GetValue("DebugMode:UseFixedFile", false); if (useFixedFile) { _logger.LogWarning($"[{request.Partner}] ⚠️ DEBUG MODE: File created at {filePath}, ACMS call skipped"); // ファイル情報をログ出力 var fileInfo = new FileInfo(filePath); _logger.LogInformation($"[{request.Partner}] File size: {fileInfo.Length} bytes, Created: {fileInfo.CreationTime}"); // デバッグ用: ファイルの先頭部分を表示 try { byte[] firstBytes = File.ReadAllBytes(filePath).Take(100).ToArray(); string firstBytesHex = BitConverter.ToString(firstBytes).Replace("-", " "); _logger.LogDebug($"[{request.Partner}] File content (first 100 bytes HEX): {firstBytesHex}"); } catch (Exception ex) { _logger.LogWarning(ex, $"[{request.Partner}] Could not read file for debug output"); } // デバッグモード: ACMS呼び出しをスキップして成功レスポンスを返却 return new SendResponse { Success = true, SentAt = DateTime.Now, ReturnCode = 0 // 成功 }; } _logger.LogInformation($"[{request.Partner}] Starting ACMSCallSend: uid={config.Uid}, fid={config.SendFid}, fileName={Path.GetFileName(filePath)}, path={filePath}"); // ACMSCallSend実行(接続→送信→切断を一括実行) var msgBuffer = AcmsApiWrapper.CreateMessageBuffer(); int ret = AcmsApiWrapper.ACMSCallSend( config.Uid, config.SendFid, filePath, request.Cycle, msgBuffer); // ユーザーフレンドリーなメッセージに変換 string userMessage = AcmsErrorMessageMapper.GetUserFriendlyMessage(ret, msgBuffer); // ログには元のrawメッセージも記録(診断用) string rawMessage = AcmsApiWrapper.DecodeMessage(msgBuffer); _logger.LogInformation($"[{request.Partner}] ACMSCallSend returned: ret={ret}, raw={rawMessage}, user={userMessage}"); if (ret != 0) { return new SendResponse { Success = false, ReturnCode = ret, Error = userMessage }; } return new SendResponse { Success = true, SentAt = DateTime.Now, ReturnCode = ret }; } catch (Exception ex) { _logger.LogError(ex, $"[{request.Partner}] SendFile error: {ex.Message}"); return new SendResponse { Success = false, ReturnCode = -1, Error = ex.Message }; } } }