| @@ -54,7 +54,6 @@ | |||
| <div asp-validation-summary="ModelOnly" class="text-danger mb-3"></div> | |||
| <!-- 隱藏欄位:系統傳入的資料 --> | |||
| <input type="hidden" asp-for="InvoiceData.Identifier" /> | |||
| <input type="hidden" asp-for="InvoiceData.TransDateTime" /> | |||
| <input type="hidden" asp-for="InvoiceData.TransAmount" /> | |||
| <input type="hidden" asp-for="InvoiceData.DeviceID" /> | |||
| @@ -63,6 +62,21 @@ | |||
| <input type="hidden" asp-for="InvoiceData.OrderID" /> | |||
| <input type="hidden" asp-for="InvoiceData.TaxType" /> | |||
| @{ | |||
| if (string.IsNullOrWhiteSpace(Model.InvoiceData.Identifier)) | |||
| { | |||
| Model.InvoiceData.Identifier = Model.CompanyTaxId; | |||
| } | |||
| } | |||
| <div class="mb-3"> | |||
| <label asp-for="InvoiceData.Identifier" class="form-label">開發票者統編</label> | |||
| <input asp-for="InvoiceData.Identifier" | |||
| class="form-control" | |||
| maxlength="8" /> | |||
| <span asp-validation-for="InvoiceData.Identifier" class="text-danger"></span> | |||
| </div> | |||
| <div class="alert alert-info mb-4"> | |||
| <strong>注意:</strong>以下三個選項只能填寫其中一個 | |||
| </div> | |||
| @@ -7,20 +7,24 @@ using System.Text.Json; | |||
| namespace Altob.NtuInvoiceGateway.Pages; | |||
| [IgnoreAntiforgeryToken] // 允許外部系統 POST JSON 請求 | |||
| public class InvoiceModel : PageModel | |||
| { | |||
| private readonly ILogger<InvoiceModel> _logger; | |||
| private readonly IHttpClientFactory _httpClientFactory; | |||
| private readonly CompanyInfo _companyInfo; | |||
| private readonly InvoiceApiOptions _invoiceApiOptions; | |||
| public InvoiceModel( | |||
| ILogger<InvoiceModel> logger, | |||
| IHttpClientFactory httpClientFactory, | |||
| IOptions<CompanyInfo> companyInfo) | |||
| IOptions<CompanyInfo> companyInfo, | |||
| IOptions<InvoiceApiOptions> invoiceApiOptions) | |||
| { | |||
| _logger = logger; | |||
| _httpClientFactory = httpClientFactory; | |||
| _companyInfo = companyInfo.Value; | |||
| _invoiceApiOptions = invoiceApiOptions.Value; | |||
| } | |||
| [BindProperty] | |||
| @@ -35,55 +39,49 @@ public class InvoiceModel : PageModel | |||
| public string DisplayTransDateTime { get; set; } = string.Empty; | |||
| public string DisplayTransAmount { get; set; } = string.Empty; | |||
| public void OnGet( | |||
| string? identifier = null, | |||
| string? transDateTime = null, | |||
| string? transAmount = null, | |||
| string? deviceID = null, | |||
| string? email = null, | |||
| string? carrierID = null, | |||
| string? locationID = null, | |||
| string? carPlateNum = null, | |||
| string? orderID = null, | |||
| string? buyerIdentifier = null, | |||
| // string? loveCode = null, | |||
| string? taxType = null) | |||
| public void OnGet() | |||
| { | |||
| // 從查詢字串接收系統轉頁傳來的資料 | |||
| if (!string.IsNullOrEmpty(identifier)) | |||
| InvoiceData.Identifier = identifier; | |||
| if (!string.IsNullOrEmpty(transDateTime)) | |||
| { | |||
| InvoiceData.TransDateTime = transDateTime; | |||
| DisplayTransDateTime = transDateTime; | |||
| } | |||
| if (!string.IsNullOrEmpty(transAmount)) | |||
| { | |||
| InvoiceData.TransAmount = transAmount; | |||
| DisplayTransAmount = transAmount; | |||
| } | |||
| if (!string.IsNullOrEmpty(deviceID)) | |||
| InvoiceData.DeviceID = deviceID; | |||
| if (!string.IsNullOrEmpty(email)) | |||
| InvoiceData.Email = email; | |||
| if (!string.IsNullOrEmpty(carrierID)) | |||
| InvoiceData.CarrierID = carrierID; | |||
| if (!string.IsNullOrEmpty(locationID)) | |||
| InvoiceData.LocationID = locationID; | |||
| if (!string.IsNullOrEmpty(carPlateNum)) | |||
| InvoiceData.CarPlateNum = carPlateNum; | |||
| if (!string.IsNullOrEmpty(orderID)) | |||
| InvoiceData.OrderID = orderID; | |||
| if (!string.IsNullOrEmpty(buyerIdentifier)) | |||
| InvoiceData.BuyerIdentifier = buyerIdentifier; | |||
| // if (!string.IsNullOrEmpty(loveCode)) | |||
| // InvoiceData.LoveCode = loveCode; | |||
| if (!string.IsNullOrEmpty(taxType)) | |||
| InvoiceData.TaxType = taxType; | |||
| // GET 方法保留為空,主要接收方式改為 POST JSON | |||
| } | |||
| public async Task<IActionResult> OnPostAsync() | |||
| { | |||
| // 檢查是否為 JSON 請求(外部系統轉頁) | |||
| if (Request.ContentType?.Contains("application/json") == true) | |||
| { | |||
| try | |||
| { | |||
| // 從 Request Body 讀取 JSON 資料 | |||
| using var reader = new StreamReader(Request.Body); | |||
| var jsonContent = await reader.ReadToEndAsync(); | |||
| var jsonData = JsonSerializer.Deserialize<InvoiceRequest>(jsonContent, new JsonSerializerOptions | |||
| { | |||
| PropertyNamingPolicy = JsonNamingPolicy.CamelCase, | |||
| PropertyNameCaseInsensitive = true | |||
| }); | |||
| if (jsonData != null) | |||
| { | |||
| InvoiceData = jsonData; | |||
| DisplayTransDateTime = InvoiceData.TransDateTime; | |||
| DisplayTransAmount = InvoiceData.TransAmount; | |||
| ModelState.Clear(); // ensure Razor uses the JSON payload values | |||
| _logger.LogInformation("Received JSON redirect for OrderID: {OrderID}", InvoiceData.OrderID); | |||
| } | |||
| } | |||
| catch (Exception ex) | |||
| { | |||
| _logger.LogError(ex, "Error parsing JSON request"); | |||
| ErrorMessage = "接收轉頁資料時發生錯誤"; | |||
| } | |||
| // 顯示表單讓用戶填寫 | |||
| return Page(); | |||
| } | |||
| // 處理表單提交 | |||
| // 保留發票資訊顯示 | |||
| DisplayTransDateTime = InvoiceData.TransDateTime; | |||
| DisplayTransAmount = InvoiceData.TransAmount; | |||
| @@ -117,26 +115,54 @@ public class InvoiceModel : PageModel | |||
| return Page(); | |||
| } | |||
| if (string.IsNullOrWhiteSpace(_invoiceApiOptions.Endpoint)) | |||
| { | |||
| ErrorMessage = "未設定外部發票 API 的位址"; | |||
| _logger.LogError("Invoice API endpoint is not configured"); | |||
| return Page(); | |||
| } | |||
| try | |||
| { | |||
| // 呼叫 TODO API(這裡使用規格中的範例 API) | |||
| // http://192.168.110.72:22055/api/Intella/invoiceInfo | |||
| // { | |||
| // "identifier": "12345678", | |||
| // "transDateTime": "2026/01/08 12:00:00", | |||
| // "transAmount": "30", | |||
| // "deviceID": "test", | |||
| // "locationID": "1", | |||
| // "carPlateNum": "ABC1235", | |||
| // "orderID": "260109_1", | |||
| // "email": "", | |||
| // "carrierID": "/ab12345", | |||
| // "buyerIdentifier": "", | |||
| // "taxType": "1", | |||
| // "loveCode": "" | |||
| // } | |||
| var httpClient = _httpClientFactory.CreateClient(); | |||
| var jsonContent = JsonSerializer.Serialize(InvoiceData, new JsonSerializerOptions | |||
| var requestBody = JsonSerializer.Serialize(InvoiceData, new JsonSerializerOptions | |||
| { | |||
| PropertyNamingPolicy = JsonNamingPolicy.CamelCase | |||
| }); | |||
| var content = new StringContent(jsonContent, Encoding.UTF8, "application/json"); | |||
| var content = new StringContent(requestBody, Encoding.UTF8, "application/json"); | |||
| // TODO: 替換為實際的 API 端點 | |||
| var response = await httpClient.PostAsync("YOUR_API_ENDPOINT_HERE", content); | |||
| var response = await httpClient.PostAsync(_invoiceApiOptions.Endpoint, content); | |||
| var responseContent = await response.Content.ReadAsStringAsync(); | |||
| var apiResponse = JsonSerializer.Deserialize<InvoiceResponse>(responseContent, new JsonSerializerOptions | |||
| { | |||
| PropertyNamingPolicy = JsonNamingPolicy.CamelCase | |||
| PropertyNameCaseInsensitive = true | |||
| }); | |||
| if (!response.IsSuccessStatusCode) | |||
| { | |||
| ErrorMessage = $"提交失敗:{apiResponse?.msg ?? response.StatusCode.ToString()}"; | |||
| _logger.LogWarning("Invoice submission failed with HTTP {StatusCode} for OrderID: {OrderID}", response.StatusCode, InvoiceData.OrderID); | |||
| return Page(); | |||
| } | |||
| if (apiResponse?.msgCode == "0000") | |||
| { | |||
| SuccessMessage = "發票資訊提交成功!"; | |||
| @@ -1,4 +1,5 @@ | |||
| @page | |||
| @model TestPostInvoiceModel | |||
| @{ | |||
| ViewData["Title"] = "POST 轉頁測試"; | |||
| Layout = null; | |||
| @@ -25,12 +26,20 @@ | |||
| <div class="row mb-3"> | |||
| <div class="col-md-6"> | |||
| <label class="form-label">開發票者統編 (Identifier) *</label> | |||
| <input type="text" class="form-control" name="InvoiceData.Identifier" value="12345678" required> | |||
| <input type="text" | |||
| class="form-control" | |||
| name="InvoiceData.Identifier" | |||
| value="@(string.IsNullOrWhiteSpace(Model.CompanyTaxId) ? "" : Model.CompanyTaxId)" | |||
| required> | |||
| <small class="text-muted">8位數字</small> | |||
| </div> | |||
| <div class="col-md-6"> | |||
| <label class="form-label">發票交易日期 (TransDateTime) *</label> | |||
| <input type="text" class="form-control" name="InvoiceData.TransDateTime" value="2024-01-15 14:30:00" required> | |||
| <input type="text" | |||
| class="form-control" | |||
| name="InvoiceData.TransDateTime" | |||
| value="@Model.CurrentTransDateTime" | |||
| required> | |||
| </div> | |||
| </div> | |||
| @@ -42,26 +51,26 @@ | |||
| </div> | |||
| <div class="col-md-6"> | |||
| <label class="form-label">設備代號 (DeviceID) *</label> | |||
| <input type="text" class="form-control" name="InvoiceData.DeviceID" value="DEVICE001" required> | |||
| <input type="text" class="form-control" name="InvoiceData.DeviceID" value="NtuInvoiceGateway" required> | |||
| </div> | |||
| </div> | |||
| <div class="row mb-3"> | |||
| <div class="col-md-6"> | |||
| <label class="form-label">停車場站代號 (LocationID) *</label> | |||
| <input type="text" class="form-control" name="InvoiceData.LocationID" value="A" maxlength="1" required> | |||
| <input type="text" class="form-control" name="InvoiceData.LocationID" value="1" maxlength="1" required> | |||
| <small class="text-muted">1個字元</small> | |||
| </div> | |||
| <div class="col-md-6"> | |||
| <label class="form-label">車牌號碼 (CarPlateNum) *</label> | |||
| <input type="text" class="form-control" name="InvoiceData.CarPlateNum" value="ABC1234" required> | |||
| <input type="text" class="form-control" name="InvoiceData.CarPlateNum" value="9M1234" required> | |||
| </div> | |||
| </div> | |||
| <div class="row mb-3"> | |||
| <div class="col-md-6"> | |||
| <label class="form-label">訂單編號 (OrderID) *</label> | |||
| <input type="text" class="form-control" name="InvoiceData.OrderID" value="ORDER20240115001" required> | |||
| <input type="text" class="form-control" name="InvoiceData.OrderID" value="@Model.CurrentOrderNo" required> | |||
| </div> | |||
| <div class="col-md-6"> | |||
| <label class="form-label">稅別 (TaxType) *</label> | |||
| @@ -114,6 +123,21 @@ | |||
| </ul> | |||
| </div> | |||
| <hr class="my-4"> | |||
| <div class="card border-primary"> | |||
| <div class="card-header bg-primary text-white"> | |||
| <h5>JSON POST 測試(符合規格要求)</h5> | |||
| </div> | |||
| <div class="card-body"> | |||
| <p class="text-success"><strong>✓ 此方式符合英特拉規格:Method: POST, 資料格式: JSON</strong></p> | |||
| <button type="button" class="btn btn-primary btn-lg w-100" onclick="testJsonPost()"> | |||
| 發送 JSON POST 請求到發票頁面 | |||
| </button> | |||
| <div id="jsonResult" class="mt-3"></div> | |||
| </div> | |||
| </div> | |||
| <hr> | |||
| <div class="card bg-light"> | |||
| @@ -127,5 +151,55 @@ | |||
| </div> | |||
| <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script> | |||
| <script> | |||
| async function testJsonPost() { | |||
| const resultDiv = document.getElementById('jsonResult'); | |||
| resultDiv.innerHTML = '<div class="alert alert-info">發送中...</div>'; | |||
| const form = document.querySelector('form'); | |||
| if (!form) { | |||
| resultDiv.innerHTML = '<div class="alert alert-danger">找不到表單元素</div>'; | |||
| return; | |||
| } | |||
| const formData = new FormData(form); | |||
| const jsonData = { | |||
| identifier: formData.get('InvoiceData.Identifier')?.toString() ?? '', | |||
| transDateTime: formData.get('InvoiceData.TransDateTime')?.toString() ?? '', | |||
| transAmount: formData.get('InvoiceData.TransAmount')?.toString() ?? '', | |||
| deviceID: formData.get('InvoiceData.DeviceID')?.toString() ?? '', | |||
| email: formData.get('InvoiceData.Email')?.toString() ?? '', | |||
| carrierID: formData.get('InvoiceData.CarrierID')?.toString() ?? '', | |||
| locationID: formData.get('InvoiceData.LocationID')?.toString() ?? '', | |||
| carPlateNum: formData.get('InvoiceData.CarPlateNum')?.toString() ?? '', | |||
| orderID: formData.get('InvoiceData.OrderID')?.toString() ?? '', | |||
| buyerIdentifier: formData.get('InvoiceData.BuyerIdentifier')?.toString() ?? '', | |||
| loveCode: '', | |||
| taxType: formData.get('InvoiceData.TaxType')?.toString() ?? '' | |||
| }; | |||
| try { | |||
| const response = await fetch('/Invoice', { | |||
| method: 'POST', | |||
| headers: { | |||
| 'Content-Type': 'application/json' | |||
| }, | |||
| body: JSON.stringify(jsonData) | |||
| }); | |||
| if (response.ok) { | |||
| // JSON POST 成功,直接顯示返回的 HTML 頁面 | |||
| const html = await response.text(); | |||
| document.open(); | |||
| document.write(html); | |||
| document.close(); | |||
| } else { | |||
| resultDiv.innerHTML = `<div class="alert alert-danger">請求失敗: ${response.status}</div>`; | |||
| } | |||
| } catch (error) { | |||
| resultDiv.innerHTML = `<div class="alert alert-danger">發生錯誤: ${error.message}</div>`; | |||
| } | |||
| } | |||
| </script> | |||
| </body> | |||
| </html> | |||
| @@ -1,9 +1,23 @@ | |||
| using System; | |||
| using Altob.NtuInvoiceGateway.Models; | |||
| using Microsoft.AspNetCore.Mvc.RazorPages; | |||
| using Microsoft.Extensions.Options; | |||
| namespace Altob.NtuInvoiceGateway.Pages; | |||
| public class TestPostInvoiceModel : PageModel | |||
| { | |||
| public string CompanyTaxId { get; } | |||
| public string CurrentTransDateTime { get; } | |||
| public string CurrentOrderNo { get; } | |||
| public TestPostInvoiceModel(IOptions<CompanyInfo> companyInfo) | |||
| { | |||
| CompanyTaxId = companyInfo.Value.TaxId; | |||
| CurrentTransDateTime = DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss"); | |||
| CurrentOrderNo = DateTime.Now.ToString("ORDERyyyyMMddHHmmssfff"); | |||
| } | |||
| public void OnGet() | |||
| { | |||
| } | |||
| @@ -7,6 +7,8 @@ builder.Services.AddHttpClient(); | |||
| // Configure company info | |||
| builder.Services.Configure<Altob.NtuInvoiceGateway.Models.CompanyInfo>( | |||
| builder.Configuration.GetSection("CompanyInfo")); | |||
| builder.Services.Configure<Altob.NtuInvoiceGateway.Models.InvoiceApiOptions>( | |||
| builder.Configuration.GetSection("InvoiceApi")); | |||
| var app = builder.Build(); | |||
| @@ -28,4 +30,4 @@ app.MapStaticAssets(); | |||
| app.MapRazorPages() | |||
| .WithStaticAssets(); | |||
| app.Run(); | |||
| app.Run(); | |||
| @@ -5,5 +5,8 @@ | |||
| "Default": "Information", | |||
| "Microsoft.AspNetCore": "Warning" | |||
| } | |||
| }, | |||
| "InvoiceApi": { | |||
| "Endpoint": "http://192.168.110.72:22055/api/Intella/invoiceInfo" | |||
| } | |||
| } | |||
| @@ -9,5 +9,8 @@ | |||
| "CompanyInfo": { | |||
| "Name": "國立台灣大學臨時停車場", | |||
| "TaxId": "18384226" | |||
| }, | |||
| "InvoiceApi": { | |||
| "Endpoint": "http://192.168.110.72:22055/api/Intella/invoiceInfo" | |||
| } | |||
| } | |||