提供給台大案英特拉線上繳費填寫發票資訊用
25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.

341 satır
14KB

  1. using Microsoft.AspNetCore.Mvc;
  2. using Microsoft.AspNetCore.Mvc.RazorPages;
  3. using Microsoft.Extensions.Options;
  4. using Microsoft.Extensions.Caching.Memory;
  5. using Altob.NtuInvoiceGateway.Models;
  6. using System.Text;
  7. using System.Text.Json;
  8. using System.Linq;
  9. namespace Altob.NtuInvoiceGateway.Pages;
  10. [IgnoreAntiforgeryToken] // 允許外部系統 POST JSON 請求
  11. public class InvoiceModel : PageModel
  12. {
  13. private readonly ILogger<InvoiceModel> _logger;
  14. private readonly IHttpClientFactory _httpClientFactory;
  15. private readonly IMemoryCache _memoryCache;
  16. private readonly CompanyInfo _companyInfo;
  17. private readonly InvoiceApiOptions _invoiceApiOptions;
  18. private readonly DonateCodeApiOptions _donateCodeApiOptions;
  19. private const string ServiceName = "NtuInvoiceGateway";
  20. private const string DonateCodeCacheKey = "DonateCodeList";
  21. public InvoiceModel(
  22. ILogger<InvoiceModel> logger,
  23. IHttpClientFactory httpClientFactory,
  24. IMemoryCache memoryCache,
  25. IOptions<CompanyInfo> companyInfo,
  26. IOptions<InvoiceApiOptions> invoiceApiOptions,
  27. IOptions<DonateCodeApiOptions> donateCodeApiOptions)
  28. {
  29. _logger = logger;
  30. _httpClientFactory = httpClientFactory;
  31. _memoryCache = memoryCache;
  32. _companyInfo = companyInfo.Value;
  33. _invoiceApiOptions = invoiceApiOptions.Value;
  34. _donateCodeApiOptions = donateCodeApiOptions.Value;
  35. }
  36. [BindProperty(SupportsGet = true)]
  37. public InvoiceRequest InvoiceData { get; set; } = new();
  38. public string? ErrorMessage { get; set; }
  39. [TempData]
  40. public string? SuccessMessage { get; set; }
  41. public string CompanyName => _companyInfo.Name;
  42. public string CompanyTaxId => _companyInfo.TaxId;
  43. // 顯示用的發票資訊(唯讀)
  44. public string DisplayTransDateTime { get; set; } = string.Empty;
  45. public string DisplayTransAmount { get; set; } = string.Empty;
  46. public void OnGet()
  47. {
  48. RestoreDisplayValuesFromTempData();
  49. NormalizeInvoiceData();
  50. // 如果有 GET 參數,設置顯示資訊
  51. DisplayTransDateTime = InvoiceData.TransDateTime;
  52. DisplayTransAmount = InvoiceData.TransAmount;
  53. }
  54. public async Task<IActionResult> OnGetDonateCodesAsync()
  55. {
  56. var actionName = nameof(OnGetDonateCodesAsync);
  57. try
  58. {
  59. // 檢查 Cache 是否有資料
  60. if (_memoryCache.TryGetValue(DonateCodeCacheKey, out List<DonateGroup>? cachedData) && cachedData != null)
  61. {
  62. _logger.LogInformation("{ServiceName} - {ActionName} returning cached donate codes, count: {Count}",
  63. ServiceName, actionName, cachedData.Count);
  64. return new JsonResult(cachedData);
  65. }
  66. // Cache 中沒有資料,從 API 取得
  67. if (string.IsNullOrWhiteSpace(_donateCodeApiOptions.Endpoint))
  68. {
  69. _logger.LogError("{ServiceName} - {ActionName} DonateCode API endpoint is not configured", ServiceName, actionName);
  70. return new JsonResult(new List<DonateGroup>()) { StatusCode = 500 };
  71. }
  72. _logger.LogInformation("{ServiceName} - {ActionName} fetching donate codes from API: {Endpoint}",
  73. ServiceName, actionName, _donateCodeApiOptions.Endpoint);
  74. var httpClient = _httpClientFactory.CreateClient();
  75. var response = await httpClient.GetAsync(_donateCodeApiOptions.Endpoint);
  76. if (!response.IsSuccessStatusCode)
  77. {
  78. _logger.LogError("{ServiceName} - {ActionName} failed to fetch donate codes, status: {StatusCode}",
  79. ServiceName, actionName, response.StatusCode);
  80. return new JsonResult(new List<DonateGroup>()) { StatusCode = (int)response.StatusCode };
  81. }
  82. var responseContent = await response.Content.ReadAsStringAsync();
  83. var donateCodes = JsonSerializer.Deserialize<List<DonateGroup>>(responseContent, new JsonSerializerOptions
  84. {
  85. PropertyNameCaseInsensitive = true
  86. });
  87. if (donateCodes != null && donateCodes.Count > 0)
  88. {
  89. // 按照 Seq 欄位排序
  90. var sortedDonateCodes = donateCodes.OrderBy(d => d.Seq).ToList();
  91. // 將資料存入 Cache,有效期限 7 天
  92. var cacheOptions = new MemoryCacheEntryOptions
  93. {
  94. AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(7)
  95. };
  96. _memoryCache.Set(DonateCodeCacheKey, sortedDonateCodes, cacheOptions);
  97. _logger.LogInformation("{ServiceName} - {ActionName} cached {Count} donate codes for 7 days",
  98. ServiceName, actionName, sortedDonateCodes.Count);
  99. return new JsonResult(sortedDonateCodes);
  100. }
  101. _logger.LogWarning("{ServiceName} - {ActionName} received empty donate code list from API", ServiceName, actionName);
  102. return new JsonResult(new List<DonateGroup>());
  103. }
  104. catch (Exception ex)
  105. {
  106. _logger.LogError(ex, "{ServiceName} - {ActionName} error fetching donate codes", ServiceName, actionName);
  107. return new JsonResult(new List<DonateGroup>()) { StatusCode = 500 };
  108. }
  109. }
  110. public async Task<IActionResult> OnPostAsync()
  111. {
  112. NormalizeInvoiceData();
  113. var actionName = nameof(OnPostAsync);
  114. _logger.LogInformation("{ServiceName} - {ActionName} received JSON redirect payload {@InvoiceData}",
  115. ServiceName, actionName, InvoiceData);
  116. // 檢查是否為 JSON 請求(外部系統轉頁)
  117. if (Request.ContentType?.Contains("application/json") == true)
  118. {
  119. try
  120. {
  121. // 從 Request Body 讀取 JSON 資料
  122. using var reader = new StreamReader(Request.Body);
  123. var jsonContent = await reader.ReadToEndAsync();
  124. _logger.LogInformation("{ServiceName} - {ActionName} JSON payload content: {JsonPayload}", ServiceName, actionName, jsonContent);
  125. var jsonData = JsonSerializer.Deserialize<InvoiceRequest>(jsonContent, new JsonSerializerOptions
  126. {
  127. PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
  128. PropertyNameCaseInsensitive = true
  129. });
  130. if (jsonData != null)
  131. {
  132. InvoiceData = jsonData;
  133. NormalizeInvoiceData();
  134. DisplayTransDateTime = InvoiceData.TransDateTime;
  135. DisplayTransAmount = InvoiceData.TransAmount;
  136. ModelState.Clear(); // ensure Razor uses the JSON payload values
  137. _logger.LogInformation("{ServiceName} - {ActionName} received redirect for OrderID: {OrderID}", ServiceName, actionName, InvoiceData.OrderID);
  138. }
  139. }
  140. catch (Exception ex)
  141. {
  142. _logger.LogError(ex, "{ServiceName} - {ActionName} error parsing JSON request", ServiceName, actionName);
  143. ErrorMessage = "接收轉頁資料時發生錯誤";
  144. }
  145. }
  146. // 顯示表單讓用戶填寫
  147. return Page();
  148. }
  149. public async Task<IActionResult> OnPostToEndPointAsync()
  150. {
  151. NormalizeInvoiceData();
  152. // 處理表單提交
  153. // 保留發票資訊顯示
  154. DisplayTransDateTime = InvoiceData.TransDateTime;
  155. DisplayTransAmount = InvoiceData.TransAmount;
  156. var actionName = nameof(OnPostToEndPointAsync);
  157. _logger.LogInformation("{ServiceName} - {ActionName} received invoice submission payload {@InvoiceData}",
  158. ServiceName, actionName, InvoiceData);
  159. // 判斷是否為初始轉頁(三個選項都沒填寫)
  160. bool isInitialRedirect = string.IsNullOrEmpty(InvoiceData.Email) &&
  161. string.IsNullOrEmpty(InvoiceData.CarrierID) &&
  162. string.IsNullOrEmpty(InvoiceData.BuyerIdentifier) &&
  163. string.IsNullOrEmpty(InvoiceData.LoveCode);
  164. // 如果是初始轉頁,直接顯示表單讓用戶填寫
  165. if (isInitialRedirect)
  166. {
  167. return Page();
  168. }
  169. if (!ModelState.IsValid)
  170. {
  171. var invalidFields = ModelState
  172. .Where(entry => entry.Value?.Errors.Count > 0)
  173. .Select(entry =>
  174. {
  175. var fieldName = string.IsNullOrEmpty(entry.Key) ? "表單" : entry.Key;
  176. var messages = string.Join("、", entry.Value!.Errors.Select(e => e.ErrorMessage));
  177. return $"{fieldName}:{messages}";
  178. });
  179. ErrorMessage = "資料驗證失敗,請檢查欄位輸入";
  180. _logger.LogWarning("{ServiceName} - {ActionName} validation failed: {ValidationErrors}", ServiceName, actionName, string.Join(" | ", invalidFields));
  181. return Page();
  182. }
  183. // 驗證 Email, CarrierID, BuyerIdentifier 至少填寫一個
  184. int filledCount = 0;
  185. if (!string.IsNullOrEmpty(InvoiceData.Email)) filledCount++;
  186. if (!string.IsNullOrEmpty(InvoiceData.CarrierID)) filledCount++;
  187. if (!string.IsNullOrEmpty(InvoiceData.BuyerIdentifier)) filledCount++;
  188. if (!string.IsNullOrEmpty(InvoiceData.LoveCode)) filledCount++;
  189. if (filledCount == 0)
  190. {
  191. ErrorMessage = "Email、手機條碼、購買者統編至少需填寫一個";
  192. return Page();
  193. }
  194. if (string.IsNullOrWhiteSpace(_invoiceApiOptions.Endpoint))
  195. {
  196. ErrorMessage = "未設定外部發票 API 的位址";
  197. _logger.LogError("{ServiceName} - {ActionName} Invoice API endpoint is not configured", ServiceName, actionName);
  198. return Page();
  199. }
  200. try
  201. {
  202. // 呼叫 TODO API(這裡使用規格中的範例 API)
  203. // http://192.168.110.72:22055/api/Intella/invoiceInfo
  204. // {
  205. // "identifier": "12345678",
  206. // "transDateTime": "2026/01/08 12:00:00",
  207. // "transAmount": "30",
  208. // "deviceID": "test",
  209. // "locationID": "1",
  210. // "carPlateNum": "ABC1235",
  211. // "orderID": "260109_1",
  212. // "email": "",
  213. // "carrierID": "/ab12345",
  214. // "buyerIdentifier": "",
  215. // "taxType": "1",
  216. // "loveCode": ""
  217. // }
  218. var httpClient = _httpClientFactory.CreateClient();
  219. var requestBody = JsonSerializer.Serialize(InvoiceData, new JsonSerializerOptions
  220. {
  221. PropertyNamingPolicy = JsonNamingPolicy.CamelCase
  222. });
  223. _logger.LogInformation("{ServiceName} - {ActionName} sending API payload: {RequestBody}",
  224. ServiceName, actionName, requestBody);
  225. var content = new StringContent(requestBody, Encoding.UTF8, "application/json");
  226. var response = await httpClient.PostAsync(_invoiceApiOptions.Endpoint, content);
  227. var responseContent = await response.Content.ReadAsStringAsync();
  228. _logger.LogInformation("{ServiceName} - {ActionName} received API response status {StatusCode} body: {ResponseBody}",
  229. ServiceName, actionName, response.StatusCode, responseContent);
  230. var apiResponse = JsonSerializer.Deserialize<InvoiceResponse>(responseContent, new JsonSerializerOptions
  231. {
  232. PropertyNameCaseInsensitive = true
  233. });
  234. if (!response.IsSuccessStatusCode)
  235. {
  236. ErrorMessage = $"送出失敗:{apiResponse?.msg ?? response.StatusCode.ToString()}";
  237. _logger.LogWarning("{ServiceName} - {ActionName} submission failed with HTTP {StatusCode} for OrderID: {OrderID}",
  238. ServiceName, actionName, response.StatusCode, InvoiceData.OrderID);
  239. return Page();
  240. }
  241. if (apiResponse?.msgCode == "0000")
  242. {
  243. SuccessMessage = "發票資訊送出成功!";
  244. TempData[nameof(DisplayTransDateTime)] = DisplayTransDateTime;
  245. TempData[nameof(DisplayTransAmount)] = DisplayTransAmount;
  246. _logger.LogInformation("{ServiceName} - {ActionName} submitted successfully for OrderID: {OrderID}",
  247. ServiceName, actionName, InvoiceData.OrderID);
  248. return RedirectToPage();
  249. }
  250. else
  251. {
  252. ErrorMessage = $"送出失敗:{apiResponse?.msg ?? "未知錯誤"}";
  253. _logger.LogWarning("{ServiceName} - {ActionName} submission failed: {Message}", ServiceName, actionName, apiResponse?.msg);
  254. }
  255. }
  256. catch (Exception ex)
  257. {
  258. #if DEBUG
  259. ErrorMessage = $"系統錯誤:{ex.Message}";
  260. #else
  261. ErrorMessage = $"系統錯誤";
  262. #endif
  263. _logger.LogError(ex, "{ServiceName} - {ActionName} error submitting invoice for OrderID: {OrderID}",
  264. ServiceName, actionName, InvoiceData.OrderID);
  265. }
  266. return Page();
  267. }
  268. private void NormalizeInvoiceData()
  269. {
  270. InvoiceData ??= new InvoiceRequest();
  271. InvoiceData.Identifier = InvoiceData.Identifier ?? string.Empty;
  272. InvoiceData.TransDateTime = InvoiceData.TransDateTime ?? string.Empty;
  273. InvoiceData.TransAmount = InvoiceData.TransAmount ?? string.Empty;
  274. InvoiceData.DeviceID = InvoiceData.DeviceID ?? string.Empty;
  275. InvoiceData.LocationID = InvoiceData.LocationID ?? string.Empty;
  276. InvoiceData.CarPlateNum = InvoiceData.CarPlateNum ?? string.Empty;
  277. InvoiceData.OrderID = InvoiceData.OrderID ?? string.Empty;
  278. InvoiceData.TaxType = InvoiceData.TaxType ?? string.Empty;
  279. InvoiceData.Email = InvoiceData.Email ?? string.Empty;
  280. InvoiceData.CarrierID = InvoiceData.CarrierID ?? string.Empty;
  281. InvoiceData.BuyerIdentifier = InvoiceData.BuyerIdentifier ?? string.Empty;
  282. InvoiceData.LoveCode = InvoiceData.LoveCode ?? string.Empty;
  283. }
  284. private void RestoreDisplayValuesFromTempData()
  285. {
  286. if (TempData.TryGetValue(nameof(DisplayTransDateTime), out var transDateObj) &&
  287. transDateObj is string transDate)
  288. {
  289. DisplayTransDateTime = transDate;
  290. }
  291. if (TempData.TryGetValue(nameof(DisplayTransAmount), out var amountObj) &&
  292. amountObj is string amount)
  293. {
  294. DisplayTransAmount = amount;
  295. }
  296. }
  297. }