| @@ -22,11 +22,11 @@ public class InvoiceRequest | |||
| [StringLength(64, ErrorMessage = "Email長度不能超過64個字元")] | |||
| [EmailAddress(ErrorMessage = "Email格式不正確")] | |||
| public string? Email { get; set; } | |||
| public string? Email { get; set; } = string.Empty; | |||
| [StringLength(8, ErrorMessage = "手機條碼長度不能超過8個字元")] | |||
| [RegularExpression(@"^\/[A-Z0-9.+\-]{7}$", ErrorMessage = "手機條碼格式不正確,必須為8碼,第一碼為/,其餘為大寫英數字、點、加號或減號")] | |||
| public string? CarrierID { get; set; } | |||
| public string? CarrierID { get; set; } = string.Empty; | |||
| [Required(ErrorMessage = "停車場站代號為必填")] | |||
| [StringLength(1, ErrorMessage = "停車場站代號長度不能超過1個字元")] | |||
| @@ -42,10 +42,11 @@ public class InvoiceRequest | |||
| [StringLength(8, ErrorMessage = "購買者統編長度不能超過8個字元")] | |||
| [RegularExpression(@"^\d{8}$", ErrorMessage = "統編格式不正確,必須為8位數字")] | |||
| public string? BuyerIdentifier { get; set; } | |||
| public string? BuyerIdentifier { get; set; } = string.Empty; | |||
| // [StringLength(7, ErrorMessage = "愛心碼長度不能超過7個字元")] | |||
| // public string? LoveCode { get; set; } | |||
| [StringLength(6, ErrorMessage = "愛心碼長度不能超過6碼")] | |||
| [RegularExpression(@"^$|^\d{6}$", ErrorMessage = "愛心碼須為空值或6位數字")] | |||
| public string LoveCode { get; set; } = string.Empty; | |||
| [Required(ErrorMessage = "稅別為必填")] | |||
| [StringLength(1, ErrorMessage = "稅別長度不能超過1個字元")] | |||
| @@ -1,10 +0,0 @@ | |||
| @page | |||
| @model IndexModel | |||
| @{ | |||
| ViewData["Title"] = "Home page"; | |||
| } | |||
| <div class="text-center"> | |||
| <h1 class="display-4">Welcome</h1> | |||
| <p>Learn about <a href="https://learn.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p> | |||
| </div> | |||
| @@ -1,18 +0,0 @@ | |||
| using Microsoft.AspNetCore.Mvc; | |||
| using Microsoft.AspNetCore.Mvc.RazorPages; | |||
| namespace Altob.NtuInvoiceGateway.Pages; | |||
| public class IndexModel : PageModel | |||
| { | |||
| private readonly ILogger<IndexModel> _logger; | |||
| public IndexModel(ILogger<IndexModel> logger) | |||
| { | |||
| _logger = logger; | |||
| } | |||
| public void OnGet() | |||
| { | |||
| } | |||
| } | |||
| @@ -45,15 +45,16 @@ | |||
| @if (!string.IsNullOrEmpty(Model.SuccessMessage)) | |||
| { | |||
| <div class="alert alert-success alert-dismissible fade show" role="alert"> | |||
| @Model.SuccessMessage | |||
| @Model.SuccessMessage 1111 | |||
| <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> | |||
| </div> | |||
| } | |||
| <form method="post"> | |||
| <div asp-validation-summary="ModelOnly" class="text-danger mb-3"></div> | |||
| <form method="post" asp-page-handler="ToEndPoint"> | |||
| <div asp-validation-summary="All" 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" /> | |||
| @@ -62,23 +63,8 @@ | |||
| <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>以下三個選項只能填寫其中一個 | |||
| <strong>注意:</strong>以下三個選項至少需填寫一個 | |||
| </div> | |||
| <div class="row mb-3"> | |||
| @@ -123,26 +109,23 @@ | |||
| @section Scripts { | |||
| @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} | |||
| <script> | |||
| // 確保只有一個選項被填寫 | |||
| document.addEventListener('DOMContentLoaded', function() { | |||
| const exclusiveFields = [ | |||
| const requiredGroup = [ | |||
| document.querySelector('input[name="InvoiceData.Email"]'), | |||
| document.querySelector('input[name="InvoiceData.CarrierID"]'), | |||
| document.querySelector('input[name="InvoiceData.BuyerIdentifier"]') | |||
| // document.querySelector('input[name="InvoiceData.LoveCode"]') | |||
| ]; | |||
| exclusiveFields.forEach(field => { | |||
| if (field) { | |||
| field.addEventListener('input', function() { | |||
| if (this.value) { | |||
| exclusiveFields.forEach(otherField => { | |||
| if (otherField !== this) { | |||
| otherField.value = ''; | |||
| } | |||
| }); | |||
| } | |||
| }); | |||
| const form = document.querySelector('form[method="post"]'); | |||
| if (!form) { | |||
| return; | |||
| } | |||
| form.addEventListener('submit', function(event) { | |||
| const hasValue = requiredGroup.some(field => field && field.value.trim() !== ''); | |||
| if (!hasValue) { | |||
| event.preventDefault(); | |||
| alert('Email、手機條碼、購買者統編至少需填寫一項。'); | |||
| } | |||
| }); | |||
| }); | |||
| @@ -4,6 +4,7 @@ using Microsoft.Extensions.Options; | |||
| using Altob.NtuInvoiceGateway.Models; | |||
| using System.Text; | |||
| using System.Text.Json; | |||
| using System.Linq; | |||
| namespace Altob.NtuInvoiceGateway.Pages; | |||
| @@ -46,6 +47,7 @@ public class InvoiceModel : PageModel | |||
| public async Task<IActionResult> OnPostAsync() | |||
| { | |||
| NormalizeInvoiceData(); | |||
| // 檢查是否為 JSON 請求(外部系統轉頁) | |||
| if (Request.ContentType?.Contains("application/json") == true) | |||
| { | |||
| @@ -64,6 +66,7 @@ public class InvoiceModel : PageModel | |||
| if (jsonData != null) | |||
| { | |||
| InvoiceData = jsonData; | |||
| NormalizeInvoiceData(); | |||
| DisplayTransDateTime = InvoiceData.TransDateTime; | |||
| DisplayTransAmount = InvoiceData.TransAmount; | |||
| ModelState.Clear(); // ensure Razor uses the JSON payload values | |||
| @@ -76,11 +79,14 @@ public class InvoiceModel : PageModel | |||
| _logger.LogError(ex, "Error parsing JSON request"); | |||
| ErrorMessage = "接收轉頁資料時發生錯誤"; | |||
| } | |||
| // 顯示表單讓用戶填寫 | |||
| return Page(); | |||
| } | |||
| // 顯示表單讓用戶填寫 | |||
| return Page(); | |||
| } | |||
| public async Task<IActionResult> OnPostToEndPointAsync() | |||
| { | |||
| NormalizeInvoiceData(); | |||
| // 處理表單提交 | |||
| // 保留發票資訊顯示 | |||
| DisplayTransDateTime = InvoiceData.TransDateTime; | |||
| @@ -99,19 +105,30 @@ public class InvoiceModel : PageModel | |||
| if (!ModelState.IsValid) | |||
| { | |||
| var invalidFields = ModelState | |||
| .Where(entry => entry.Value?.Errors.Count > 0) | |||
| .Select(entry => | |||
| { | |||
| var fieldName = string.IsNullOrEmpty(entry.Key) ? "表單" : entry.Key; | |||
| var messages = string.Join("、", entry.Value!.Errors.Select(e => e.ErrorMessage)); | |||
| return $"{fieldName}:{messages}"; | |||
| }); | |||
| ErrorMessage = "資料驗證失敗,請檢查欄位輸入"; | |||
| _logger.LogWarning("Invoice submission validation failed: {ValidationErrors}", string.Join(" | ", invalidFields)); | |||
| return Page(); | |||
| } | |||
| // 驗證 Email, CarrierID, BuyerIdentifier 只能填寫其中一個 | |||
| // 驗證 Email, CarrierID, BuyerIdentifier 至少填寫一個 | |||
| int filledCount = 0; | |||
| if (!string.IsNullOrEmpty(InvoiceData.Email)) filledCount++; | |||
| if (!string.IsNullOrEmpty(InvoiceData.CarrierID)) filledCount++; | |||
| if (!string.IsNullOrEmpty(InvoiceData.BuyerIdentifier)) filledCount++; | |||
| // if (!string.IsNullOrEmpty(InvoiceData.LoveCode)) filledCount++; | |||
| if (filledCount != 1) | |||
| if (filledCount == 0) | |||
| { | |||
| ErrorMessage = "Email、手機條碼、購買者統編只能填寫其中一個"; | |||
| ErrorMessage = "Email、手機條碼、購買者統編至少需填寫一個"; | |||
| return Page(); | |||
| } | |||
| @@ -176,10 +193,32 @@ public class InvoiceModel : PageModel | |||
| } | |||
| catch (Exception ex) | |||
| { | |||
| #if DEBUG | |||
| ErrorMessage = $"系統錯誤:{ex.Message}"; | |||
| #else | |||
| ErrorMessage = $"系統錯誤"; | |||
| #endif | |||
| _logger.LogError(ex, "Error submitting invoice for OrderID: {OrderID}", InvoiceData.OrderID); | |||
| } | |||
| return Page(); | |||
| } | |||
| private void NormalizeInvoiceData() | |||
| { | |||
| InvoiceData ??= new InvoiceRequest(); | |||
| InvoiceData.Identifier = InvoiceData.Identifier ?? string.Empty; | |||
| InvoiceData.TransDateTime = InvoiceData.TransDateTime ?? string.Empty; | |||
| InvoiceData.TransAmount = InvoiceData.TransAmount ?? string.Empty; | |||
| InvoiceData.DeviceID = InvoiceData.DeviceID ?? string.Empty; | |||
| InvoiceData.Email = InvoiceData.Email ?? string.Empty; | |||
| InvoiceData.CarrierID = InvoiceData.CarrierID ?? string.Empty; | |||
| InvoiceData.LocationID = InvoiceData.LocationID ?? string.Empty; | |||
| InvoiceData.CarPlateNum = InvoiceData.CarPlateNum ?? string.Empty; | |||
| InvoiceData.OrderID = InvoiceData.OrderID ?? string.Empty; | |||
| InvoiceData.BuyerIdentifier = InvoiceData.BuyerIdentifier ?? string.Empty; | |||
| InvoiceData.LoveCode = InvoiceData.LoveCode ?? string.Empty; | |||
| InvoiceData.TaxType = InvoiceData.TaxType ?? string.Empty; | |||
| } | |||
| } | |||
| @@ -1,8 +0,0 @@ | |||
| @page | |||
| @model PrivacyModel | |||
| @{ | |||
| ViewData["Title"] = "Privacy Policy"; | |||
| } | |||
| <h1>@ViewData["Title"]</h1> | |||
| <p>Use this page to detail your site's privacy policy.</p> | |||
| @@ -1,18 +0,0 @@ | |||
| using Microsoft.AspNetCore.Mvc; | |||
| using Microsoft.AspNetCore.Mvc.RazorPages; | |||
| namespace Altob.NtuInvoiceGateway.Pages; | |||
| public class PrivacyModel : PageModel | |||
| { | |||
| private readonly ILogger<PrivacyModel> _logger; | |||
| public PrivacyModel(ILogger<PrivacyModel> logger) | |||
| { | |||
| _logger = logger; | |||
| } | |||
| public void OnGet() | |||
| { | |||
| } | |||
| } | |||
| @@ -3,7 +3,7 @@ | |||
| <head> | |||
| <meta charset="utf-8"/> | |||
| <meta name="viewport" content="width=device-width, initial-scale=1.0"/> | |||
| <title>@ViewData["Title"] - Altob.NtuInvoiceGateway</title> | |||
| <title>@ViewData["Title"]</title> | |||
| <script type="importmap"></script> | |||
| <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css"/> | |||
| <link rel="stylesheet" href="~/css/site.css" asp-append-version="true"/> | |||
| @@ -39,7 +39,7 @@ | |||
| <footer class="border-top footer text-muted"> | |||
| <div class="container"> | |||
| © 2026 - Altob.NtuInvoiceGateway - <a asp-area="" asp-page="/Privacy">Privacy</a> | |||
| © 2026 - Altob 歐特儀 | |||
| </div> | |||
| </footer> | |||
| @@ -3,7 +3,7 @@ | |||
| <head> | |||
| <meta charset="utf-8"/> | |||
| <meta name="viewport" content="width=device-width, initial-scale=1.0"/> | |||
| <title>@ViewData["Title"] - Altob.NtuInvoiceGateway</title> | |||
| <title>@ViewData["Title"]</title> | |||
| <script type="importmap"></script> | |||
| <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css"/> | |||
| <link rel="stylesheet" href="~/css/site.css" asp-append-version="true"/> | |||
| @@ -18,7 +18,7 @@ | |||
| <footer class="border-top footer text-muted mt-5"> | |||
| <div class="container text-center"> | |||
| © 2026 - Altob.NtuInvoiceGateway | |||
| © 2026 - Altob ¼Ú¯S»ö | |||
| </div> | |||
| </footer> | |||
| @@ -51,7 +51,7 @@ | |||
| </div> | |||
| <div class="col-md-6"> | |||
| <label class="form-label">設備代號 (DeviceID) *</label> | |||
| <input type="text" class="form-control" name="InvoiceData.DeviceID" value="NtuInvoiceGateway" required> | |||
| <input type="text" class="form-control" name="InvoiceData.DeviceID" value="NtuInvGateway" required> | |||
| </div> | |||
| </div> | |||
| @@ -15,7 +15,7 @@ public class TestPostInvoiceModel : PageModel | |||
| { | |||
| CompanyTaxId = companyInfo.Value.TaxId; | |||
| CurrentTransDateTime = DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss"); | |||
| CurrentOrderNo = DateTime.Now.ToString("ORDERyyyyMMddHHmmssfff"); | |||
| CurrentOrderNo = string.Format($"Order{DateTime.Now:yyyyMMddHHmmss}"); | |||
| } | |||
| public void OnGet() | |||
| @@ -1,12 +0,0 @@ | |||
| { | |||
| "DetailedErrors": true, | |||
| "Logging": { | |||
| "LogLevel": { | |||
| "Default": "Information", | |||
| "Microsoft.AspNetCore": "Warning" | |||
| } | |||
| }, | |||
| "InvoiceApi": { | |||
| "Endpoint": "http://192.168.110.72:22055/api/Intella/invoiceInfo" | |||
| } | |||
| } | |||
| @@ -1,8 +1,8 @@ | |||
| { | |||
| "Logging": { | |||
| "LogLevel": { | |||
| "Default": "Information", | |||
| "Microsoft.AspNetCore": "Warning" | |||
| "Default": "Debug", | |||
| "Microsoft.AspNetCore": "Information" | |||
| } | |||
| }, | |||
| "AllowedHosts": "*", | |||