| @@ -22,11 +22,11 @@ public class InvoiceRequest | |||||
| [StringLength(64, ErrorMessage = "Email長度不能超過64個字元")] | [StringLength(64, ErrorMessage = "Email長度不能超過64個字元")] | ||||
| [EmailAddress(ErrorMessage = "Email格式不正確")] | [EmailAddress(ErrorMessage = "Email格式不正確")] | ||||
| public string? Email { get; set; } | |||||
| public string? Email { get; set; } = string.Empty; | |||||
| [StringLength(8, ErrorMessage = "手機條碼長度不能超過8個字元")] | [StringLength(8, ErrorMessage = "手機條碼長度不能超過8個字元")] | ||||
| [RegularExpression(@"^\/[A-Z0-9.+\-]{7}$", ErrorMessage = "手機條碼格式不正確,必須為8碼,第一碼為/,其餘為大寫英數字、點、加號或減號")] | [RegularExpression(@"^\/[A-Z0-9.+\-]{7}$", ErrorMessage = "手機條碼格式不正確,必須為8碼,第一碼為/,其餘為大寫英數字、點、加號或減號")] | ||||
| public string? CarrierID { get; set; } | |||||
| public string? CarrierID { get; set; } = string.Empty; | |||||
| [Required(ErrorMessage = "停車場站代號為必填")] | [Required(ErrorMessage = "停車場站代號為必填")] | ||||
| [StringLength(1, ErrorMessage = "停車場站代號長度不能超過1個字元")] | [StringLength(1, ErrorMessage = "停車場站代號長度不能超過1個字元")] | ||||
| @@ -42,10 +42,11 @@ public class InvoiceRequest | |||||
| [StringLength(8, ErrorMessage = "購買者統編長度不能超過8個字元")] | [StringLength(8, ErrorMessage = "購買者統編長度不能超過8個字元")] | ||||
| [RegularExpression(@"^\d{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 = "稅別為必填")] | [Required(ErrorMessage = "稅別為必填")] | ||||
| [StringLength(1, ErrorMessage = "稅別長度不能超過1個字元")] | [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)) | @if (!string.IsNullOrEmpty(Model.SuccessMessage)) | ||||
| { | { | ||||
| <div class="alert alert-success alert-dismissible fade show" role="alert"> | <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> | <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> | ||||
| </div> | </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.TransDateTime" /> | ||||
| <input type="hidden" asp-for="InvoiceData.TransAmount" /> | <input type="hidden" asp-for="InvoiceData.TransAmount" /> | ||||
| <input type="hidden" asp-for="InvoiceData.DeviceID" /> | <input type="hidden" asp-for="InvoiceData.DeviceID" /> | ||||
| @@ -62,23 +63,8 @@ | |||||
| <input type="hidden" asp-for="InvoiceData.OrderID" /> | <input type="hidden" asp-for="InvoiceData.OrderID" /> | ||||
| <input type="hidden" asp-for="InvoiceData.TaxType" /> | <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"> | <div class="alert alert-info mb-4"> | ||||
| <strong>注意:</strong>以下三個選項只能填寫其中一個 | |||||
| <strong>注意:</strong>以下三個選項至少需填寫一個 | |||||
| </div> | </div> | ||||
| <div class="row mb-3"> | <div class="row mb-3"> | ||||
| @@ -123,26 +109,23 @@ | |||||
| @section Scripts { | @section Scripts { | ||||
| @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} | @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} | ||||
| <script> | <script> | ||||
| // 確保只有一個選項被填寫 | |||||
| document.addEventListener('DOMContentLoaded', function() { | document.addEventListener('DOMContentLoaded', function() { | ||||
| const exclusiveFields = [ | |||||
| const requiredGroup = [ | |||||
| document.querySelector('input[name="InvoiceData.Email"]'), | document.querySelector('input[name="InvoiceData.Email"]'), | ||||
| document.querySelector('input[name="InvoiceData.CarrierID"]'), | document.querySelector('input[name="InvoiceData.CarrierID"]'), | ||||
| document.querySelector('input[name="InvoiceData.BuyerIdentifier"]') | 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 Altob.NtuInvoiceGateway.Models; | ||||
| using System.Text; | using System.Text; | ||||
| using System.Text.Json; | using System.Text.Json; | ||||
| using System.Linq; | |||||
| namespace Altob.NtuInvoiceGateway.Pages; | namespace Altob.NtuInvoiceGateway.Pages; | ||||
| @@ -46,6 +47,7 @@ public class InvoiceModel : PageModel | |||||
| public async Task<IActionResult> OnPostAsync() | public async Task<IActionResult> OnPostAsync() | ||||
| { | { | ||||
| NormalizeInvoiceData(); | |||||
| // 檢查是否為 JSON 請求(外部系統轉頁) | // 檢查是否為 JSON 請求(外部系統轉頁) | ||||
| if (Request.ContentType?.Contains("application/json") == true) | if (Request.ContentType?.Contains("application/json") == true) | ||||
| { | { | ||||
| @@ -64,6 +66,7 @@ public class InvoiceModel : PageModel | |||||
| if (jsonData != null) | if (jsonData != null) | ||||
| { | { | ||||
| InvoiceData = jsonData; | InvoiceData = jsonData; | ||||
| NormalizeInvoiceData(); | |||||
| DisplayTransDateTime = InvoiceData.TransDateTime; | DisplayTransDateTime = InvoiceData.TransDateTime; | ||||
| DisplayTransAmount = InvoiceData.TransAmount; | DisplayTransAmount = InvoiceData.TransAmount; | ||||
| ModelState.Clear(); // ensure Razor uses the JSON payload values | ModelState.Clear(); // ensure Razor uses the JSON payload values | ||||
| @@ -76,11 +79,14 @@ public class InvoiceModel : PageModel | |||||
| _logger.LogError(ex, "Error parsing JSON request"); | _logger.LogError(ex, "Error parsing JSON request"); | ||||
| ErrorMessage = "接收轉頁資料時發生錯誤"; | ErrorMessage = "接收轉頁資料時發生錯誤"; | ||||
| } | } | ||||
| // 顯示表單讓用戶填寫 | |||||
| return Page(); | |||||
| } | } | ||||
| // 顯示表單讓用戶填寫 | |||||
| return Page(); | |||||
| } | |||||
| public async Task<IActionResult> OnPostToEndPointAsync() | |||||
| { | |||||
| NormalizeInvoiceData(); | |||||
| // 處理表單提交 | // 處理表單提交 | ||||
| // 保留發票資訊顯示 | // 保留發票資訊顯示 | ||||
| DisplayTransDateTime = InvoiceData.TransDateTime; | DisplayTransDateTime = InvoiceData.TransDateTime; | ||||
| @@ -99,19 +105,30 @@ public class InvoiceModel : PageModel | |||||
| if (!ModelState.IsValid) | 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(); | return Page(); | ||||
| } | } | ||||
| // 驗證 Email, CarrierID, BuyerIdentifier 只能填寫其中一個 | |||||
| // 驗證 Email, CarrierID, BuyerIdentifier 至少填寫一個 | |||||
| int filledCount = 0; | int filledCount = 0; | ||||
| if (!string.IsNullOrEmpty(InvoiceData.Email)) filledCount++; | if (!string.IsNullOrEmpty(InvoiceData.Email)) filledCount++; | ||||
| if (!string.IsNullOrEmpty(InvoiceData.CarrierID)) filledCount++; | if (!string.IsNullOrEmpty(InvoiceData.CarrierID)) filledCount++; | ||||
| if (!string.IsNullOrEmpty(InvoiceData.BuyerIdentifier)) filledCount++; | if (!string.IsNullOrEmpty(InvoiceData.BuyerIdentifier)) filledCount++; | ||||
| // if (!string.IsNullOrEmpty(InvoiceData.LoveCode)) filledCount++; | // if (!string.IsNullOrEmpty(InvoiceData.LoveCode)) filledCount++; | ||||
| if (filledCount != 1) | |||||
| if (filledCount == 0) | |||||
| { | { | ||||
| ErrorMessage = "Email、手機條碼、購買者統編只能填寫其中一個"; | |||||
| ErrorMessage = "Email、手機條碼、購買者統編至少需填寫一個"; | |||||
| return Page(); | return Page(); | ||||
| } | } | ||||
| @@ -176,10 +193,32 @@ public class InvoiceModel : PageModel | |||||
| } | } | ||||
| catch (Exception ex) | catch (Exception ex) | ||||
| { | { | ||||
| #if DEBUG | |||||
| ErrorMessage = $"系統錯誤:{ex.Message}"; | ErrorMessage = $"系統錯誤:{ex.Message}"; | ||||
| #else | |||||
| ErrorMessage = $"系統錯誤"; | |||||
| #endif | |||||
| _logger.LogError(ex, "Error submitting invoice for OrderID: {OrderID}", InvoiceData.OrderID); | _logger.LogError(ex, "Error submitting invoice for OrderID: {OrderID}", InvoiceData.OrderID); | ||||
| } | } | ||||
| return Page(); | 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> | <head> | ||||
| <meta charset="utf-8"/> | <meta charset="utf-8"/> | ||||
| <meta name="viewport" content="width=device-width, initial-scale=1.0"/> | <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> | <script type="importmap"></script> | ||||
| <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css"/> | <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css"/> | ||||
| <link rel="stylesheet" href="~/css/site.css" asp-append-version="true"/> | <link rel="stylesheet" href="~/css/site.css" asp-append-version="true"/> | ||||
| @@ -39,7 +39,7 @@ | |||||
| <footer class="border-top footer text-muted"> | <footer class="border-top footer text-muted"> | ||||
| <div class="container"> | <div class="container"> | ||||
| © 2026 - Altob.NtuInvoiceGateway - <a asp-area="" asp-page="/Privacy">Privacy</a> | |||||
| © 2026 - Altob 歐特儀 | |||||
| </div> | </div> | ||||
| </footer> | </footer> | ||||
| @@ -3,7 +3,7 @@ | |||||
| <head> | <head> | ||||
| <meta charset="utf-8"/> | <meta charset="utf-8"/> | ||||
| <meta name="viewport" content="width=device-width, initial-scale=1.0"/> | <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> | <script type="importmap"></script> | ||||
| <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css"/> | <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css"/> | ||||
| <link rel="stylesheet" href="~/css/site.css" asp-append-version="true"/> | <link rel="stylesheet" href="~/css/site.css" asp-append-version="true"/> | ||||
| @@ -18,7 +18,7 @@ | |||||
| <footer class="border-top footer text-muted mt-5"> | <footer class="border-top footer text-muted mt-5"> | ||||
| <div class="container text-center"> | <div class="container text-center"> | ||||
| © 2026 - Altob.NtuInvoiceGateway | |||||
| © 2026 - Altob ¼Ú¯S»ö | |||||
| </div> | </div> | ||||
| </footer> | </footer> | ||||
| @@ -51,7 +51,7 @@ | |||||
| </div> | </div> | ||||
| <div class="col-md-6"> | <div class="col-md-6"> | ||||
| <label class="form-label">設備代號 (DeviceID) *</label> | <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> | ||||
| </div> | </div> | ||||
| @@ -15,7 +15,7 @@ public class TestPostInvoiceModel : PageModel | |||||
| { | { | ||||
| CompanyTaxId = companyInfo.Value.TaxId; | CompanyTaxId = companyInfo.Value.TaxId; | ||||
| CurrentTransDateTime = DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss"); | 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() | 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": { | "Logging": { | ||||
| "LogLevel": { | "LogLevel": { | ||||
| "Default": "Information", | |||||
| "Microsoft.AspNetCore": "Warning" | |||||
| "Default": "Debug", | |||||
| "Microsoft.AspNetCore": "Information" | |||||
| } | } | ||||
| }, | }, | ||||
| "AllowedHosts": "*", | "AllowedHosts": "*", | ||||