Angular 17 & .NET 8 - LINE Pay
文章目錄
準備工作
前往Line Pay 技術支援建立 Sandbox。
信箱取得帳號資訊,LINE Pay Sandbox ID 與 PW。
使用信箱中的帳號資訊,登入 Merchant Center 取得 Channel ID 與 Channel Secret Key。
- 選擇左側選單中的「管理付款連結」,接著點擊「管理連結金鑰」。
- 點擊「查詢」,前往信箱取得驗證碼確認。
在 appsettings.json 中設定參數,並將 appsettings.json 加入 .gitignore ,以防
secret
洩漏。- 依環境設置
BaseURL
- 測試環境:https://sandbox-api-pay.line.me
- 生產環境:https://api-pay.line.me
"LinePay": { "ClientId": "paste your Channel ID here", "ClientSecret": "paste your Channel Secret Key here", "BaseURL": "https://sandbox-api-pay.line.me", "ConfirmURL": "http://localhost:7300/api/LinePay", "CancelURL": "http://localhost:4200/cust-order-detail" }
- 依環境設置
建立 LinePayService
- 注入
IConfiguration
取得參數。 - 注入
IHttpClientFactory
發送 HTTP 請求。 _jsonOptions
將 JSON 屬性名稱轉換為小駝峰。
public class LinePayService : ILinePayService
{
private readonly string _clientId;
private readonly string _clientSecret;
private readonly string _linePayBaseURL;
private readonly string _confirmURL;
private readonly string _cancelURL;
private readonly IHttpClientFactory _httpClient;
private readonly JsonSerializerOptions _jsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
public LinePayService(IConfiguration config, IHttpClientFactory httpClient)
{
_clientId = config["LinePay:ClientId"] ?? throw new NullReferenceException();
_clientSecret = config["LinePay:ClientSecret"] ?? throw new NullReferenceException();
_linePayBaseURL = config["LinePay:BaseURL"] ?? throw new NullReferenceException();
_confirmURL = config["LinePay:ConfirmURL"] ?? throw new NullReferenceException();
_cancelURL = config["LinePay:CancelURL"] ?? throw new NullReferenceException();
_httpClient = httpClient;
}
}
API authentication
發送 LINE Pay 請求前 Header 必須包含:
Content-Type
:application/json
X-LINE-ChannelId
:your Channel IDX-LINE-Authorization-Nonce
:UUID or timestampX-LINE-Authorization
:HMAC Base64 Signature
Generate Hmac Base64 Signature
private static string Encrypt(string secretKey, string value)
{
Encoding ascii = Encoding.ASCII;
HMACSHA256 hmac = new (ascii.GetBytes(secretKey));
return Convert.ToBase64String(hmac.ComputeHash(ascii.GetBytes(value)));
}
Request
發送 LINE Pay 請求,倒轉至 LINE Pay 頁面。
linePay(order: OrderHeader) {
return this._http.post(this._env.APIEndpoint.LinePay, order)
.subscribe((res: any) => location.href = res.info.paymentUrl.web);
}
透過 Request API 將使用者的購買資訊發送給 LINE Pay,以取得付款網址並倒轉至 LINE Pay 頁面,使用者即可確認訂單資訊並選擇支付方式。
接著,使用者是否成功支付的結果,將會發送至 _confirmURL
,並取得 transactionId
可用來完成支付或處理退款。
[HttpPost]
public async Task<IActionResult> PaymentRequestAsync([FromBody] OrderHeader order)
{
try
{
return Ok(await _service.RequestPaymentAsync(order));
}
catch (Exception ex)
{
Log.Error(ex.Message);
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
public async Task<PaymentResponse> RequestPaymentAsync(OrderHeader order)
{
var url = "/v3/payments/request";
var form = CreatePayment(order);
var data = JsonSerializer.Serialize(form, _jsonOptions);
var nonce = Guid.NewGuid().ToString();
var signature = Encrypt(_clientSecret, $"{_clientSecret}{url}{data}{nonce}");
var content = new StringContent(data, Encoding.UTF8, new MediaTypeHeaderValue("application/json"));
using HttpClient client = _httpClient.CreateClient();
client.DefaultRequestHeaders.Add("X-LINE-ChannelId", _clientId);
client.DefaultRequestHeaders.Add("X-LINE-Authorization-Nonce", nonce);
client.DefaultRequestHeaders.Add("X-LINE-Authorization", signature);
var response = await client.PostAsync($"{_linePayBaseURL}{url}", content);
var resContent = await response.Content.ReadAsStringAsync();
var res = JsonSerializer.Deserialize<PaymentResponse>(resContent, _jsonOptions)
?? throw new Exception("Null PaymentResponse");
if (res.ReturnCode != "0000")
{
throw new Exception($"{res.ReturnCode}:{res.ReturnMessage}");
}
return res;
}
Create Request body
構建請求資料,須注意 OrderId
不可重複。
private PaymentRequest CreatePayment(OrderHeader order)
{
var form = new PaymentRequest
{
Amount = order.OrderAmount,
OrderId = $"{order.Id}_{DateTime.Now:yy/MM/dd HH:mm:ss}"
};
var productPackageForm = new Package
{
Id = $"{order.Id}",
Name = $"{order.AppUserId}",
Amount = order.OrderAmount
};
foreach (var detail in order.OrderDetails)
{
var product = detail.Product;
if (detail != null && product != null)
{
var productForm = new Models.LinePay.Product
{
Id = $"{product.Id}",
Name = $"{product.Name}",
Quantity = detail!.Count,
Price = detail.UnitPrice
};
productPackageForm.Products.Add(productForm);
}
}
form.Packages.Add(productPackageForm);
var redirectUrls = new RedirectUrls
{
AppPackageName = "Cafe",
ConfirmUrl = _confirmURL,
CancelUrl = $"{_cancelURL}/{order.Id}"
};
form.RedirectUrls = redirectUrls;
return form;
}
Confirm
建立一個 API 端點來接收回應並取得 transactionId
和 orderId
,該 API 端點即為 _confirmURL
的值。
使用 orderId
自資料庫取得訂單資訊,並在付款成功後修改資料庫中的訂單付款資訊。
[HttpGet]
public async Task<IActionResult> PaymentConfirmAsync([FromQuery] string transactionId, [FromQuery] string orderId)
{
try
{
var oId = int.Parse(orderId.Split('_')[0]);
var order = await _orderService.FindAsync(oId);
var res = await _service.ConfirmPaymentAsync(transactionId, order.OrderAmount);
order.OrderNo = res.Info.TransactionId.ToString();
string payMethod = string.Join(",", res.Info.PayInfo.Select(p => p.Method));
order.PaymentType = $"LinePay-{payMethod}";
order.PaymentTypeChargeFee = (int)res.Info.Packages.Sum(_ => _.UserFeeAmount);
order.PaymentStatus = PaymentStatus.Paid;
order.PaymentTime = DateTime.Now;
await _orderService.UpdateAsync(order);
return Redirect($"http://localhost:4200/cust-order-detail/{order.Id}");
}
catch (Exception ex)
{
Log.Error(ex.Message);
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
public async Task<ConfirmResponse> ConfirmPaymentAsync(string transactionId, int amount)
{
string url = $"/v3/payments/{transactionId}/confirm";
string data = JsonSerializer.Serialize(new { amount, currency = "TWD" }, _jsonOptions);
var content = new StringContent(data, Encoding.UTF8, "application/json");
var nonce = Guid.NewGuid().ToString();
var signature = Encrypt(_clientSecret, $"{_clientSecret}{url}{data}{nonce}");
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-LINE-ChannelId", _clientId);
client.DefaultRequestHeaders.Add("X-LINE-Authorization-Nonce", nonce);
client.DefaultRequestHeaders.Add("X-LINE-Authorization", signature);
var response = await client.PostAsync($"{_linePayBaseURL}{url}", content);
string resContent = await response.Content.ReadAsStringAsync();
var res = JsonSerializer.Deserialize<ConfirmResponse>(resContent, _jsonOptions)
?? throw new Exception("Null PaymentResponse");
if (res.ReturnCode != "0000")
{
throw new Exception($"{res.ReturnCode}:{res.ReturnMessage}");
}
return res;
}