藍新金流 NewebPay 是國內第三方支付平台之一,金流界俗稱的紅藍綠之中的藍新金流。
第三方支付平台提供代收款及代付款等功能,幫助電商網站在網路上向消費者收款,而第三方支付平台則收取手續費做為營業收入。
藍新金流提供主流收款方式,包含線上刷卡、超商代收、ATM/WebATM 以及行動支付工具,每種收款方式的手續費各有不同,關於費率表可參考
官方說明
。
今天將會示範如何在 ASP.NET 串接藍新金流 API,執行線上交易,藍新金流提供測試環境幫助初次串接者測試,所以我會以測試環境做為示範。
藍新金流 API 文件
串接 API 的規則都寫在文件內,可以到
API 文件下載區
點擊「線上交易─幕前支付(MPG)」下載。
要串接的重要資訊都寫在此文件內,串接前一定要看一下。
API 版本會隨時間更新,如果有以前建置的 API,也要定期看一下是否要更新程式內的 API 規則。
串接 API 前準備事項
藍新金流測試環境
對於第一次串接的人來說,難免會有一些狀況發生,而且金流又有錢的問題,如果出錯導致虧損就不好了,建議先在測試環境下串接成功了,再移到正式環境再測一遍。
在 API 文件內有測試環境串接說明。
有 2 個測試網址,串接網址:
https://ccore.newebpay.com/MPG/mpg_gateway
,後台管理網址:
https://cwww.newebpay.com/
,這兩個網址比較重要。
可以先到
後台管理網址
,註冊帳號填寫個人基本資料,首次註冊需要上傳一些證件驗證,要等官方審核後,才可開立商店。
如果太久都沒有審核通過,建議直接請客服幫忙比較快,因為是測試環境,審核不一定有人處理。
當審核通過後,就可以到「商店管理」的「開立商店設定」新增資料。
當開立商店完成後,點擊商店的「詳細資料」。
對 API 會用到的資訊有「商店代號」,以及下面的「API 串接金鑰」。
在下方會找到 API 串接金鑰。
在呼叫金流 API 時,不管成功能否,都會由 API 伺服器主動回報執行結果,所以我們需要建立對外網域來接收結果,對外網域建議先有固定 IP 後,再由網域商註冊網域,然後透過 DNS 指向至網站主機。
如果需要註冊網域,可參考另一篇文章:
GoDaddy 購買網域教學-建立你的網路門牌
在開發階段使用的 localhost 網址是不能測試金流 API 的喔。
當開發好程式後,可以在對外網域的 IIS 上建立站台,指定接受網域。
開啟 Visual Studio 2022,建立新專案為「ASP.NET Core Web 應用程式 (Model-View-Controller)」。
輸入專案名稱、路徑,架構選擇「.NET 6.0」版本,按下「建立」就會建立此專案。
加入 Vue3 套件
Vue3 是前端控制欄位的框架類別庫,打開 \Views\Shared\_Layout.cshtml 檔案,在下方 JavaScript 引用增加 Vue3 類別庫語法,順序的要求要放在 jQuery 之後才行。
<script src="https://unpkg.com/vue@3"></script>
當在 Layout 加上 Vue3 引用後,我們就可以在所有的頁面使用 Vue3 語法了,此引用語法來源可參考
官方文件
。
停用 Json 回傳預設小寫 (駝峰式命名) 設定
.NET Core 在 Controller 回傳 Json 時,會將變數修改為開頭預設小寫 (駝峰式命名),這設定容易造成前端取值大小寫問題,所以我會停用此設定。
在 Program.cs 加入以下語法:
// 維持 Json 回傳大小寫與 ViewModel 相同
builder.Services.AddControllers().AddJsonOptions(options =>
options.JsonSerializerOptions.PropertyNamingPolicy = null;
IConfiguration Config = new ConfigurationBuilder().AddJsonFile("appSettings.json").Build();
// 產生測試資訊
ViewData["MerchantID"] = Config.GetSection("MerchantID").Value;
ViewData["MerchantOrderNo"] = DateTime.Now.ToString("yyyyMMddHHmmss"); //訂單編號
ViewData["ExpireDate"] = DateTime.Now.AddDays(3).ToString("yyyyMMdd"); //繳費有效期限
ViewData["ReturnURL"] = $"{Request.Scheme}://{Request.Host}{Request.Path}Home/CallbackReturn"; //支付完成返回商店網址
ViewData["CustomerURL"] = $"{Request.Scheme}://{Request.Host}{Request.Path}Home/CallbackCustomer"; //商店取號網址
ViewData["NotifyURL"] = $"{Request.Scheme}://{Request.Host}{Request.Path}Home/CallbackNotify"; //支付通知網址
ViewData["ClientBackURL"] = $"{Request.Scheme}://{Request.Host}{Request.Path}"; //返回商店網址
IConfiguration
Config
=
new
ConfigurationBuilder
(
)
.
AddJsonFile
(
"appSettings.json"
)
.
Build
(
)
;
// 產生測試資訊
ViewData
[
"MerchantID"
]
=
Config
.
GetSection
(
"MerchantID"
)
.
Value
;
ViewData
[
"MerchantOrderNo"
]
=
DateTime
.
Now
.
ToString
(
"yyyyMMddHHmmss"
)
;
//訂單編號
ViewData
[
"ExpireDate"
]
=
DateTime
.
Now
.
AddDays
(
3
)
.
ToString
(
"yyyyMMdd"
)
;
//繳費有效期限
ViewData
[
"ReturnURL"
]
=
$
"{Request.Scheme}://{Request.Host}{Request.Path}Home/CallbackReturn"
;
//支付完成返回商店網址
ViewData
[
"CustomerURL"
]
=
$
"{Request.Scheme}://{Request.Host}{Request.Path}Home/CallbackCustomer"
;
//商店取號網址
ViewData
[
"NotifyURL"
]
=
$
"{Request.Scheme}://{Request.Host}{Request.Path}Home/CallbackNotify"
;
//支付通知網址
ViewData
[
"ClientBackURL"
]
=
$
"{Request.Scheme}://{Request.Host}{Request.Path}"
;
//返回商店網址
"MerchantID": "MS000000000", //藍新金流商店代號
"HashKey": "XXXXXXXXXXXXXXXXXXXXXXXXXXXX", //藍新金流串接金鑰
"HashIV": "XXXXXXXXXXXXXXXXXXXXXXXXXX" ////藍新金流串接密鑰
<div class="col-md-6">
<label class="form-label">商店代號</label>
<input type="text" class="form-control" id="MerchantID" v-model="addForm.MerchantID">
<div class="col-md-6">
<label class="form-label">訂單編號</label>
<input type="text" class="form-control" id="MerchantOrderNo" v-model="addForm.MerchantOrderNo">
<div class="row mb-2">
<div class="col-md-6">
<label class="form-label">商品說明</label>
<input type="text" class="form-control" id="ItemDesc" v-model="addForm.ItemDesc">
<div class="col-md-6">
<label class="form-label">商品金額</label>
<input type="text" class="form-control" id="Amt" v-model="addForm.Amt">
<div class="row mb-2">
<div class="col-md-6">
<label class="form-label">繳費有效期限</label>
<input type="text" class="form-control" id="ExpireDate" v-model="addForm.ExpireDate">
<div class="col-md-6">
<label class="form-label">付款人電子信箱</label>
<input type="text" class="form-control" id="Email" v-model="addForm.Email">
<div class="row mb-2">
<div class="col-md-6">
<label class="form-label">支付完成返回網址</label>
<input type="text" class="form-control" id="ReturnURL" v-model="addForm.ReturnURL">
<div class="col-md-6">
<label class="form-label">商店取號網址</label>
<input type="text" class="form-control" id="CustomerURL" v-model="addForm.CustomerURL">
<div class="row mb-2">
<div class="col-md-6">
<label class="form-label">支付通知網址</label>
<input type="text" class="form-control" id="NotifyURL" v-model="addForm.NotifyURL">
<div class="col-md-6">
<label class="form-label">返回商店網址</label>
<input type="text" class="form-control" id="ClientBackURL" v-model="addForm.ClientBackURL">
<button type="button" class="btn btn-primary" v-on:click="SendToNewebPay('VACC')">ATM 付款</button>
@section scripts {
<script>
const app = Vue.createApp({
data() {
return {
// 表單資料
addForm: {
MerchantID: '@ViewData["MerchantID"]' //商品代號
, MerchantOrderNo: '@ViewData["MerchantOrderNo"]'
, ItemDesc: '測試商品'
, Amt: '100'
, ExpireDate: '@ViewData["ExpireDate"]'
, ReturnURL: '@ViewData["ReturnURL"]'
, CustomerURL: '@ViewData["CustomerURL"]'
, NotifyURL: '@ViewData["NotifyURL"]'
, ClientBackURL: '@ViewData["ClientBackURL"]'
, Email: '
[email protected]'
, methods: {
// 傳送至藍新金流
SendToNewebPay(ChannelID) {
var self = this;
// 組合表單資料
var postData = {};
postData['ChannelID'] = ChannelID;
postData['MerchantID'] = self.addForm.MerchantID;
postData['MerchantOrderNo'] = self.addForm.MerchantOrderNo;
postData['ItemDesc'] = self.addForm.ItemDesc;
postData['Amt'] = self.addForm.Amt;
postData['ExpireDate'] = self.addForm.ExpireDate;
postData['ReturnURL'] = self.addForm.ReturnURL;
postData['CustomerURL'] = self.addForm.CustomerURL;
postData['NotifyURL'] = self.addForm.NotifyURL;
postData['ClientBackURL'] = self.addForm.ClientBackURL;
postData['Email'] = self.addForm.Email;
// 使用 jQuery Ajax 傳送至後端
$.ajax({
url: '@Url.Content("~/Home/SendToNewebPay")',
method: 'POST',
dataType: 'json',
data: { inModel: postData, __RequestVerificationToken: $('@Html.AntiForgeryToken()').val() },
success: function(returnObj) {
// 呼叫藍新金流 API
const form = document.createElement('form');
form.method = 'post';
form.action = 'https://ccore.newebpay.com/MPG/mpg_gateway';//藍新金流驗證網址(測試環境)
for (const key in returnObj) {
if (returnObj.hasOwnProperty(key)) {
const hiddenField = document.createElement('input');
hiddenField.type = 'hidden';
hiddenField.name = key;
hiddenField.value = returnObj[key];
form.appendChild(hiddenField);
document.body.appendChild(form);
form.submit();
error: function(err) {
alert(err.status + " " + err.statusText + '\n' + err.responseText);
const vm = app.mount('#app');
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
<h1>
藍新金流串接範例
</h1>
<div
class
=
"card"
id
=
"app"
>
<div
class
=
"card-header"
>
API 欄位
</div>
<div
class
=
"card-body"
>
<div
class
=
"row mb-2"
>
<div
class
=
"col-md-6"
>
<label
class
=
"form-label"
>
商店代號
</label>
<input
type
=
"text"
class
=
"form-control"
id
=
"MerchantID"
v-model
=
"addForm.MerchantID"
>
</div>
<div
class
=
"col-md-6"
>
<label
class
=
"form-label"
>
訂單編號
</label>
<input
type
=
"text"
class
=
"form-control"
id
=
"MerchantOrderNo"
v-model
=
"addForm.MerchantOrderNo"
>
</div>
</div>
<div
class
=
"row mb-2"
>
<div
class
=
"col-md-6"
>
<label
class
=
"form-label"
>
商品說明
</label>
<input
type
=
"text"
class
=
"form-control"
id
=
"ItemDesc"
v-model
=
"addForm.ItemDesc"
>
</div>
<div
class
=
"col-md-6"
>
<label
class
=
"form-label"
>
商品金額
</label>
<input
type
=
"text"
class
=
"form-control"
id
=
"Amt"
v-model
=
"addForm.Amt"
>
</div>
</div>
<div
class
=
"row mb-2"
>
<div
class
=
"col-md-6"
>
<label
class
=
"form-label"
>
繳費有效期限
</label>
<input
type
=
"text"
class
=
"form-control"
id
=
"ExpireDate"
v-model
=
"addForm.ExpireDate"
>
</div>
<div
class
=
"col-md-6"
>
<label
class
=
"form-label"
>
付款人電子信箱
</label>
<input
type
=
"text"
class
=
"form-control"
id
=
"Email"
v-model
=
"addForm.Email"
>
</div>
</div>
<div
class
=
"row mb-2"
>
<div
class
=
"col-md-6"
>
<label
class
=
"form-label"
>
支付完成返回網址
</label>
<input
type
=
"text"
class
=
"form-control"
id
=
"ReturnURL"
v-model
=
"addForm.ReturnURL"
>
</div>
<div
class
=
"col-md-6"
>
<label
class
=
"form-label"
>
商店取號網址
</label>
<input
type
=
"text"
class
=
"form-control"
id
=
"CustomerURL"
v-model
=
"addForm.CustomerURL"
>
</div>
</div>
<div
class
=
"row mb-2"
>
<div
class
=
"col-md-6"
>
<label
class
=
"form-label"
>
支付通知網址
</label>
<input
type
=
"text"
class
=
"form-control"
id
=
"NotifyURL"
v-model
=
"addForm.NotifyURL"
>
</div>
<div
class
=
"col-md-6"
>
<label
class
=
"form-label"
>
返回商店網址
</label>
<input
type
=
"text"
class
=
"form-control"
id
=
"ClientBackURL"
v-model
=
"addForm.ClientBackURL"
>
</div>
</div>
<button
type
=
"button"
class
=
"btn btn-primary"
v
-
on
:
click
=
"SendToNewebPay('VACC')"
>
ATM 付款
</button>
</div>
</div>
@
section
scripts
{
<script>
const
app
=
Vue
.
createApp
(
{
data
(
)
{
return
{
// 表單資料
addForm
:
{
MerchantID
:
'@ViewData["MerchantID"]'
//商品代號
,
MerchantOrderNo
:
'@ViewData["MerchantOrderNo"]'
,
ItemDesc
:
'測試商品'
,
Amt
:
'100'
,
ExpireDate
:
'@ViewData["ExpireDate"]'
,
ReturnURL
:
'@ViewData["ReturnURL"]'
,
CustomerURL
:
'@ViewData["CustomerURL"]'
,
NotifyURL
:
'@ViewData["NotifyURL"]'
,
ClientBackURL
:
'@ViewData["ClientBackURL"]'
}
}
}
,
methods
:
{
// 傳送至藍新金流
SendToNewebPay
(
ChannelID
)
{
var
self
=
this
;
// 組合表單資料
var
postData
=
{
}
;
postData
[
'ChannelID'
]
=
ChannelID
;
postData
[
'MerchantID'
]
=
self
.
addForm
.
MerchantID
;
postData
[
'MerchantOrderNo'
]
=
self
.
addForm
.
MerchantOrderNo
;
postData
[
'ItemDesc'
]
=
self
.
addForm
.
ItemDesc
;
postData
[
'Amt'
]
=
self
.
addForm
.
Amt
;
postData
[
'ExpireDate'
]
=
self
.
addForm
.
ExpireDate
;
postData
[
'ReturnURL'
]
=
self
.
addForm
.
ReturnURL
;
postData
[
'CustomerURL'
]
=
self
.
addForm
.
CustomerURL
;
postData
[
'NotifyURL'
]
=
self
.
addForm
.
NotifyURL
;
postData
[
'ClientBackURL'
]
=
self
.
addForm
.
ClientBackURL
;
postData
[
'Email'
]
=
self
.
addForm
.
Email
;
// 使用 jQuery Ajax 傳送至後端
$
.
ajax
(
{
url
:
'@Url.Content("~/Home/SendToNewebPay")'
,
method
:
'POST'
,
dataType
:
'json'
,
data
:
{
inModel
:
postData
,
__RequestVerificationToken
:
$
(
'@Html.AntiForgeryToken()'
)
.
val
(
)
}
,
success
:
function
(
returnObj
)
{
// 呼叫藍新金流 API
const
form
=
document
.
createElement
(
'form'
)
;
form
.
method
=
'post'
;
form
.
action
=
'https://ccore.newebpay.com/MPG/mpg_gateway'
;
//藍新金流驗證網址(測試環境)
for
(
const
key
in
returnObj
)
{
if
(
returnObj
.
hasOwnProperty
(
key
)
)
{
const
hiddenField
=
document
.
createElement
(
'input'
)
;
hiddenField
.
type
=
'hidden'
;
hiddenField
.
name
=
key
;
hiddenField
.
value
=
returnObj
[
key
]
;
form
.
appendChild
(
hiddenField
)
;
}
}
document
.
body
.
appendChild
(
form
)
;
form
.
submit
(
)
;
}
,
error
:
function
(
err
)
{
alert
(
err
.
status
+
" "
+
err
.
statusText
+
'\n'
+
err
.
responseText
)
;
}
}
)
;
}
}
}
)
;
const
vm
=
app
.
mount
(
'#app'
)
;
</script>
}
這裡設計了表單畫面,還有一個執行方法,按下方法
SendToNewebPay(ChannelID)
會呼叫至 HomeController 的
SendToNewebPay()
方法。
此頁面執行後的畫面是。
傳送至藍新金流 API
打開 \Controllers\HomeController.cs,這裡要新增一個方法,接收 View 的
SendToNewebPay()
呼叫。
加入以下語法:
/// <returns></returns>
[ValidateAntiForgeryToken]
public IActionResult SendToNewebPay(SendToNewebPayIn inModel)
SendToNewebPayOut outModel = new SendToNewebPayOut();
// 藍新金流線上付款
//交易欄位
List<KeyValuePair<string, string>> TradeInfo = new List<KeyValuePair<string, string>>();
// 商店代號
TradeInfo.Add(new KeyValuePair<string, string>("MerchantID", inModel.MerchantID));
// 回傳格式
TradeInfo.Add(new KeyValuePair<string, string>("RespondType", "String"));
// TimeStamp
TradeInfo.Add(new KeyValuePair<string, string>("TimeStamp", DateTimeOffset.Now.ToOffset(new TimeSpan(8, 0, 0)).ToUnixTimeSeconds().ToString()));
// 串接程式版本
TradeInfo.Add(new KeyValuePair<string, string>("Version", "2.0"));
// 商店訂單編號
TradeInfo.Add(new KeyValuePair<string, string>("MerchantOrderNo", inModel.MerchantOrderNo));
// 訂單金額
TradeInfo.Add(new KeyValuePair<string, string>("Amt", inModel.Amt));
// 商品資訊
TradeInfo.Add(new KeyValuePair<string, string>("ItemDesc", inModel.ItemDesc));
// 繳費有效期限(適用於非即時交易)
TradeInfo.Add(new KeyValuePair<string, string>("ExpireDate", inModel.ExpireDate));
// 支付完成返回商店網址
TradeInfo.Add(new KeyValuePair<string, string>("ReturnURL", inModel.ReturnURL));
// 支付通知網址
TradeInfo.Add(new KeyValuePair<string, string>("NotifyURL", inModel.NotifyURL));
// 商店取號網址
TradeInfo.Add(new KeyValuePair<string, string>("CustomerURL", inModel.CustomerURL));
// 支付取消返回商店網址
TradeInfo.Add(new KeyValuePair<string, string>("ClientBackURL", inModel.ClientBackURL));
// 付款人電子信箱
TradeInfo.Add(new KeyValuePair<string, string>("Email", inModel.Email));
// 付款人電子信箱 是否開放修改(1=可修改 0=不可修改)
TradeInfo.Add(new KeyValuePair<string, string>("EmailModify", "0"));
//信用卡 付款
if (inModel.ChannelID == "CREDIT")
TradeInfo.Add(new KeyValuePair<string, string>("CREDIT", "1"));
//ATM 付款
if (inModel.ChannelID == "VACC")
TradeInfo.Add(new KeyValuePair<string, string>("VACC", "1"));
string TradeInfoParam = string.Join("&", TradeInfo.Select(x => $"{x.Key}={x.Value}"));
// API 傳送欄位
// 商店代號
outModel.MerchantID = inModel.MerchantID;
// 串接程式版本
outModel.Version = "2.0";
//交易資料 AES 加解密
IConfiguration Config = new ConfigurationBuilder().AddJsonFile("appSettings.json").Build();
string HashKey = Config.GetSection("HashKey").Value;//API 串接金鑰
string HashIV = Config.GetSection("HashIV").Value;//API 串接密碼
string TradeInfoEncrypt = EncryptAESHex(TradeInfoParam, HashKey, HashIV);
outModel.TradeInfo = TradeInfoEncrypt;
//交易資料 SHA256 加密
outModel.TradeSha = EncryptSHA256($"HashKey={HashKey}&{TradeInfoEncrypt}&HashIV={HashIV}");
return Json(outModel);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
/// <summary>
/// 傳送訂單至藍新金流
/// </summary>
/// <param name="inModel"></param>
/// <returns></returns>
[
ValidateAntiForgeryToken
]
public
IActionResult
SendToNewebPay
(
SendToNewebPayIn
inModel
)
{
SendToNewebPayOut
outModel
=
new
SendToNewebPayOut
(
)
;
// 藍新金流線上付款
//交易欄位
List
<
KeyValuePair
<
string
,
string
>>
TradeInfo
=
new
List
<
KeyValuePair
<
string
,
string
>>
(
)
;
// 商店代號
TradeInfo
.
Add
(
new
KeyValuePair
<
string
,
string
>
(
"MerchantID"
,
inModel
.
MerchantID
)
)
;
// 回傳格式
TradeInfo
.
Add
(
new
KeyValuePair
<
string
,
string
>
(
"RespondType"
,
"String"
)
)
;
// TimeStamp
TradeInfo
.
Add
(
new
KeyValuePair
<
string
,
string
>
(
"TimeStamp"
,
DateTimeOffset
.
Now
.
ToOffset
(
new
TimeSpan
(
8
,
0
,
0
)
)
.
ToUnixTimeSeconds
(
)
.
ToString
(
)
)
)
;
// 串接程式版本
TradeInfo
.
Add
(
new
KeyValuePair
<
string
,
string
>
(
"Version"
,
"2.0"
)
)
;
// 商店訂單編號
TradeInfo
.
Add
(
new
KeyValuePair
<
string
,
string
>
(
"MerchantOrderNo"
,
inModel
.
MerchantOrderNo
)
)
;
// 訂單金額
TradeInfo
.
Add
(
new
KeyValuePair
<
string
,
string
>
(
"Amt"
,
inModel
.
Amt
)
)
;
// 商品資訊
TradeInfo
.
Add
(
new
KeyValuePair
<
string
,
string
>
(
"ItemDesc"
,
inModel
.
ItemDesc
)
)
;
// 繳費有效期限(適用於非即時交易)
TradeInfo
.
Add
(
new
KeyValuePair
<
string
,
string
>
(
"ExpireDate"
,
inModel
.
ExpireDate
)
)
;
// 支付完成返回商店網址
TradeInfo
.
Add
(
new
KeyValuePair
<
string
,
string
>
(
"ReturnURL"
,
inModel
.
ReturnURL
)
)
;
// 支付通知網址
TradeInfo
.
Add
(
new
KeyValuePair
<
string
,
string
>
(
"NotifyURL"
,
inModel
.
NotifyURL
)
)
;
// 商店取號網址
TradeInfo
.
Add
(
new
KeyValuePair
<
string
,
string
>
(
"CustomerURL"
,
inModel
.
CustomerURL
)
)
;
// 支付取消返回商店網址
TradeInfo
.
Add
(
new
KeyValuePair
<
string
,
string
>
(
"ClientBackURL"
,
inModel
.
ClientBackURL
)
)
;
// 付款人電子信箱
TradeInfo
.
Add
(
new
KeyValuePair
<
string
,
string
>
(
"Email"
,
inModel
.
Email
)
)
;
// 付款人電子信箱 是否開放修改(1=可修改 0=不可修改)
TradeInfo
.
Add
(
new
KeyValuePair
<
string
,
string
>
(
"EmailModify"
,
"0"
)
)
;
//信用卡 付款
if
(
inModel
.
ChannelID
==
"CREDIT"
)
{
TradeInfo
.
Add
(
new
KeyValuePair
<
string
,
string
>
(
"CREDIT"
,
"1"
)
)
;
}
//ATM 付款
if
(
inModel
.
ChannelID
==
"VACC"
)
{
TradeInfo
.
Add
(
new
KeyValuePair
<
string
,
string
>
(
"VACC"
,
"1"
)
)
;
}
string
TradeInfoParam
=
string
.
Join
(
"&"
,
TradeInfo
.
Select
(
x
=
>
$
"{x.Key}={x.Value}"
)
)
;
// API 傳送欄位
// 商店代號
outModel
.
MerchantID
=
inModel
.
MerchantID
;
// 串接程式版本
outModel
.
Version
=
"2.0"
;
//交易資料 AES 加解密
IConfiguration
Config
=
new
ConfigurationBuilder
(
)
.
AddJsonFile
(
"appSettings.json"
)
.
Build
(
)
;
string
HashKey
=
Config
.
GetSection
(
"HashKey"
)
.
Value
;
//API 串接金鑰
string
HashIV
=
Config
.
GetSection
(
"HashIV"
)
.
Value
;
//API 串接密碼
string
TradeInfoEncrypt
=
EncryptAESHex
(
TradeInfoParam
,
HashKey
,
HashIV
)
;
outModel
.
TradeInfo
=
TradeInfoEncrypt
;
//交易資料 SHA256 加密
outModel
.
TradeSha
=
EncryptSHA256
(
$
"HashKey={HashKey}&{TradeInfoEncrypt}&HashIV={HashIV}"
)
;
return
Json
(
outModel
)
;
}
這裡接收到 View 的表單後,就會組合 API 資料進行加密,等加密後會回傳至前端 View 再傳送至藍新金流 API Server。
藍新金流使用兩者加密方法,一種是 AES 可逆的加密法,另一種是 SHA256 不可逆雜湊運算
藍新金流 API 只接收 5 個欄位,可參考文件:
其中的 TradeInfo 欄位會包含訂單主要資訊,但會經過加密後才傳送出去。
補充語法內用到的加解密方法:
/// <param name="source">加密前字串</param>
/// <param name="cryptoKey">加密金鑰</param>
/// <param name="cryptoIV">cryptoIV</param>
/// <returns>加密後的字串</returns>
public string EncryptAESHex(string source, string cryptoKey, string cryptoIV)
string result = string.Empty;
if (!string.IsNullOrEmpty(source))
var encryptValue = EncryptAES(Encoding.UTF8.GetBytes(source), cryptoKey, cryptoIV);
if (encryptValue != null)
result = BitConverter.ToString(encryptValue)?.Replace("-", string.Empty)?.ToLower();
return result;
/// <summary>
/// 字串加密AES
/// </summary>
/// <param name="source">加密前字串</param>
/// <param name="cryptoKey">加密金鑰</param>
/// <param name="cryptoIV">cryptoIV</param>
/// <returns>加密後字串</returns>
public byte[] EncryptAES(byte[] source, string cryptoKey, string cryptoIV)
byte[] dataKey = Encoding.UTF8.GetBytes(cryptoKey);
byte[] dataIV = Encoding.UTF8.GetBytes(cryptoIV);
using (var aes = System.Security.Cryptography.Aes.Create())
aes.Mode = System.Security.Cryptography.CipherMode.CBC;
aes.Padding = System.Security.Cryptography.PaddingMode.PKCS7;
aes.Key = dataKey;
aes.IV = dataIV;
using (var encryptor = aes.CreateEncryptor())
return encryptor.TransformFinalBlock(source, 0, source.Length);
/// <summary>
/// 字串加密SHA256
/// </summary>
/// <param name="source">加密前字串</param>
/// <returns>加密後字串</returns>
public string EncryptSHA256(string source)
string result = string.Empty;
using (System.Security.Cryptography.SHA256 algorithm = System.Security.Cryptography.SHA256.Create())
var hash = algorithm.ComputeHash(Encoding.UTF8.GetBytes(source));
if (hash != null)
result = BitConverter.ToString(hash)?.Replace("-", string.Empty)?.ToUpper();
return result;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
/// <summary>
/// 加密後再轉 16 進制字串
/// </summary>
/// <param name="source">加密前字串</param>
/// <param name="cryptoKey">加密金鑰</param>
/// <param name="cryptoIV">cryptoIV</param>
/// <returns>加密後的字串</returns>
public
string
EncryptAESHex
(
string
source
,
string
cryptoKey
,
string
cryptoIV
)
{
string
result
=
string
.
Empty
;
if
(
!
string
.
IsNullOrEmpty
(
source
)
)
{
var
encryptValue
=
EncryptAES
(
Encoding
.
UTF8
.
GetBytes
(
source
)
,
cryptoKey
,
cryptoIV
)
;
if
(
encryptValue
!=
null
)
{
result
=
BitConverter
.
ToString
(
encryptValue
)
?
.
Replace
(
"-"
,
string
.
Empty
)
?
.
ToLower
(
)
;
}
}
return
result
;
}
/// <summary>
/// 字串加密AES
/// </summary>
/// <param name="source">加密前字串</param>
/// <param name="cryptoKey">加密金鑰</param>
/// <param name="cryptoIV">cryptoIV</param>
/// <returns>加密後字串</returns>
public
byte
[
]
EncryptAES
(
byte
[
]
source
,
string
cryptoKey
,
string
cryptoIV
)
{
byte
[
]
dataKey
=
Encoding
.
UTF8
.
GetBytes
(
cryptoKey
)
;
byte
[
]
dataIV
=
Encoding
.
UTF8
.
GetBytes
(
cryptoIV
)
;
using
(
var
aes
=
System
.
Security
.
Cryptography
.
Aes
.
Create
(
)
)
{
aes
.
Mode
=
System
.
Security
.
Cryptography
.
CipherMode
.
CBC
;
aes
.
Padding
=
System
.
Security
.
Cryptography
.
PaddingMode
.
PKCS7
;
aes
.
Key
=
dataKey
;
aes
.
IV
=
dataIV
;
using
(
var
encryptor
=
aes
.
CreateEncryptor
(
)
)
{
return
encryptor
.
TransformFinalBlock
(
source
,
0
,
source
.
Length
)
;
}
}
}
/// <summary>
/// 字串加密SHA256
/// </summary>
/// <param name="source">加密前字串</param>
/// <returns>加密後字串</returns>
public
string
EncryptSHA256
(
string
source
)
{
string
result
=
string
.
Empty
;
using
(
System
.
Security
.
Cryptography
.
SHA256
algorithm
=
System
.
Security
.
Cryptography
.
SHA256
.
Create
(
)
)
{
var
hash
=
algorithm
.
ComputeHash
(
Encoding
.
UTF8
.
GetBytes
(
source
)
)
;
if
(
hash
!=
null
)
{
result
=
BitConverter
.
ToString
(
hash
)
?
.
Replace
(
"-"
,
string
.
Empty
)
?
.
ToUpper
(
)
;
}
}
return
result
;
}
ViewModel 欄位
這裡要建一個 ViewModel 類別,來定義 Controller 與 View 之間的欄位定義,在 Models 按右鍵加入一個類別,取名為「HomeViewModel」。
在類別內加入 Controller 會用到的欄位:
public string ChannelID { get; set; }
public string MerchantID { get; set; }
public string MerchantOrderNo { get; set; }
public string ItemDesc { get; set; }
public string Amt { get; set; }
public string ExpireDate { get; set; }
public string ReturnURL { get; set; }
public string CustomerURL { get; set; }
public string NotifyURL { get; set; }
public string ClientBackURL { get; set; }
public string Email { get; set; }
public class SendToNewebPayOut
public string MerchantID { get; set; }
public string Version { get; set; }
public string TradeInfo { get; set; }
public string TradeSha { get; set; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public
class
SendToNewebPayIn
{
public
string
ChannelID
{
get
;
set
;
}
public
string
MerchantID
{
get
;
set
;
}
public
string
MerchantOrderNo
{
get
;
set
;
}
public
string
ItemDesc
{
get
;
set
;
}
public
string
Amt
{
get
;
set
;
}
public
string
ExpireDate
{
get
;
set
;
}
public
string
ReturnURL
{
get
;
set
;
}
public
string
CustomerURL
{
get
;
set
;
}
public
string
NotifyURL
{
get
;
set
;
}
public
string
ClientBackURL
{
get
;
set
;
}
public
string
Email
{
get
;
set
;
}
}
public
class
SendToNewebPayOut
{
public
string
MerchantID
{
get
;
set
;
}
public
string
Version
{
get
;
set
;
}
public
string
TradeInfo
{
get
;
set
;
}
public
string
TradeSha
{
get
;
set
;
}
}
API 返回網址接收方法
我們在前面有設定 3 個 API 返回網址,這是會從藍新金流主動呼叫的網址,所以我們要建立接收方法來取得回傳資料。
我的接收位置同樣設定在 HomeController 內,所以要新增這 3 個接收網址。
加入支付完成返回商店網址方法
在送出訂單 API 時會跳轉至藍新金流付款畫面,當使用者完成付款動作時,藍新金流會回傳呼叫我們的「支付完成返回商店網址」,這裡會接收使用者付款的狀態,我們可以從回傳資訊內查詢使用者是否已付款。
同樣在 HomeController 內加入此語法:
foreach (var item in Request.Form)
receive.AppendLine(item.Key + "=" + item.Value + "<br>");
ViewData["ReceiveObj"] = receive.ToString();
// 解密訊息
IConfiguration Config = new ConfigurationBuilder().AddJsonFile("appSettings.json").Build();
string HashKey = Config.GetSection("HashKey").Value;//API 串接金鑰
string HashIV = Config.GetSection("HashIV").Value;//API 串接密碼
string TradeInfoDecrypt = DecryptAESHex(Request.Form["TradeInfo"], HashKey, HashIV);
NameValueCollection decryptTradeCollection = HttpUtility.ParseQueryString(TradeInfoDecrypt);
receive.Length = 0;
foreach (String key in decryptTradeCollection.AllKeys)
receive.AppendLine(key + "=" + decryptTradeCollection[key] + "<br>");
ViewData["TradeInfo"] = receive.ToString();
return View();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/// <summary>
/// 支付完成返回網址
/// </summary>
/// <returns></returns>
public
IActionResult
CallbackReturn
(
)
{
// 接收參數
StringBuilder
receive
=
new
StringBuilder
(
)
;
foreach
(
var
item
in
Request
.
Form
)
{
receive
.
AppendLine
(
item
.
Key
+
"="
+
item
.
Value
+
"<br>"
)
;
}
ViewData
[
"ReceiveObj"
]
=
receive
.
ToString
(
)
;
// 解密訊息
IConfiguration
Config
=
new
ConfigurationBuilder
(
)
.
AddJsonFile
(
"appSettings.json"
)
.
Build
(
)
;
string
HashKey
=
Config
.
GetSection
(
"HashKey"
)
.
Value
;
//API 串接金鑰
string
HashIV
=
Config
.
GetSection
(
"HashIV"
)
.
Value
;
//API 串接密碼
string
TradeInfoDecrypt
=
DecryptAESHex
(
Request
.
Form
[
"TradeInfo"
]
,
HashKey
,
HashIV
)
;
NameValueCollection
decryptTradeCollection
=
HttpUtility
.
ParseQueryString
(
TradeInfoDecrypt
)
;
receive
.
Length
=
0
;
foreach
(
String
key
in
decryptTradeCollection
.
AllKeys
)
{
receive
.
AppendLine
(
key
+
"="
+
decryptTradeCollection
[
key
]
+
"<br>"
)
;
}
ViewData
[
"TradeInfo"
]
=
receive
.
ToString
(
)
;
return
View
(
)
;
}
/// <param name="source">加密前字串</param>
/// <param name="cryptoKey">加密金鑰</param>
/// <param name="cryptoIV">cryptoIV</param>
/// <returns>解密後的字串</returns>
public string DecryptAESHex(string source, string cryptoKey, string cryptoIV)
string result = string.Empty;
if (!string.IsNullOrEmpty(source))
// 將 16 進制字串 轉為 byte[] 後
byte[] sourceBytes = ToByteArray(source);
if (sourceBytes != null)
// 使用金鑰解密後,轉回 加密前 value
result = Encoding.UTF8.GetString(DecryptAES(sourceBytes, cryptoKey, cryptoIV)).Trim();
return result;
/// <summary>
/// 將16進位字串轉換為byteArray
/// </summary>
/// <param name="source">欲轉換之字串</param>
/// <returns></returns>
public byte[] ToByteArray(string source)
byte[] result = null;
if (!string.IsNullOrWhiteSpace(source))
var outputLength = source.Length / 2;
var output = new byte[outputLength];
for (var i = 0; i < outputLength; i++)
output[i] = Convert.ToByte(source.Substring(i * 2, 2), 16);
result = output;
return result;
/// <summary>
/// 字串解密AES
/// </summary>
/// <param name="source">解密前字串</param>
/// <param name="cryptoKey">解密金鑰</param>
/// <param name="cryptoIV">cryptoIV</param>
/// <returns>解密後字串</returns>
public static byte[] DecryptAES(byte[] source, string cryptoKey, string cryptoIV)
byte[] dataKey = Encoding.UTF8.GetBytes(cryptoKey);
byte[] dataIV = Encoding.UTF8.GetBytes(cryptoIV);
using (var aes = System.Security.Cryptography.Aes.Create())
aes.Mode = System.Security.Cryptography.CipherMode.CBC;
// 智付通無法直接用PaddingMode.PKCS7,會跳"填補無效,而且無法移除。"
// 所以改為PaddingMode.None並搭配RemovePKCS7Padding
aes.Padding = System.Security.Cryptography.PaddingMode.None;
aes.Key = dataKey;
aes.IV = dataIV;
using (var decryptor = aes.CreateDecryptor())
byte[] data = decryptor.TransformFinalBlock(source, 0, source.Length);
int iLength = data[data.Length - 1];
var output = new byte[data.Length - iLength];
Buffer.BlockCopy(data, 0, output, 0, output.Length);
return output;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/// <summary>
/// 16 進制字串解密
/// </summary>
/// <param name="source">加密前字串</param>
/// <param name="cryptoKey">加密金鑰</param>
/// <param name="cryptoIV">cryptoIV</param>
/// <returns>解密後的字串</returns>
public
string
DecryptAESHex
(
string
source
,
string
cryptoKey
,
string
cryptoIV
)
{
string
result
=
string
.
Empty
;
if
(
!
string
.
IsNullOrEmpty
(
source
)
)
{
// 將 16 進制字串 轉為 byte[] 後
byte
[
]
sourceBytes
=
ToByteArray
(
source
)
;
if
(
sourceBytes
!=
null
)
{
// 使用金鑰解密後,轉回 加密前 value
result
=
Encoding
.
UTF8
.
GetString
(
DecryptAES
(
sourceBytes
,
cryptoKey
,
cryptoIV
)
)
.
Trim
(
)
;
}
}
return
result
;
}
/// <summary>
/// 將16進位字串轉換為byteArray
/// </summary>
/// <param name="source">欲轉換之字串</param>
/// <returns></returns>
public
byte
[
]
ToByteArray
(
string
source
)
{
byte
[
]
result
=
null
;
if
(
!
string
.
IsNullOrWhiteSpace
(
source
)
)
{
var
outputLength
=
source
.
Length
/
2
;
var
output
=
new
byte
[
outputLength
]
;
for
(
var
i
=
0
;
i
<
outputLength
;
i
++
)
{
output
[
i
]
=
Convert
.
ToByte
(
source
.
Substring
(
i
*
2
,
2
)
,
16
)
;
}
result
=
output
;
}
return
result
;
}
/// <summary>
/// 字串解密AES
/// </summary>
/// <param name="source">解密前字串</param>
/// <param name="cryptoKey">解密金鑰</param>
/// <param name="cryptoIV">cryptoIV</param>
/// <returns>解密後字串</returns>
public
static
byte
[
]
DecryptAES
(
byte
[
]
source
,
string
cryptoKey
,
string
cryptoIV
)
{
byte
[
]
dataKey
=
Encoding
.
UTF8
.
GetBytes
(
cryptoKey
)
;
byte
[
]
dataIV
=
Encoding
.
UTF8
.
GetBytes
(
cryptoIV
)
;
using
(
var
aes
=
System
.
Security
.
Cryptography
.
Aes
.
Create
(
)
)
{
aes
.
Mode
=
System
.
Security
.
Cryptography
.
CipherMode
.
CBC
;
// 智付通無法直接用PaddingMode.PKCS7,會跳"填補無效,而且無法移除。"
// 所以改為PaddingMode.None並搭配RemovePKCS7Padding
aes
.
Padding
=
System
.
Security
.
Cryptography
.
PaddingMode
.
None
;
aes
.
Key
=
dataKey
;
aes
.
IV
=
dataIV
;
using
(
var
decryptor
=
aes
.
CreateDecryptor
(
)
)
{
byte
[
]
data
=
decryptor
.
TransformFinalBlock
(
source
,
0
,
source
.
Length
)
;
int
iLength
=
data
[
data
.
Length
-
1
]
;
var
output
=
new
byte
[
data
.
Length
-
iLength
]
;
Buffer
.
BlockCopy
(
data
,
0
,
output
,
0
,
output
.
Length
)
;
return
output
;
}
}
}
foreach (var item in Request.Form)
receive.AppendLine(item.Key + "=" + item.Value + "<br>");
ViewData["ReceiveObj"] = receive.ToString();
// 解密訊息
IConfiguration Config = new ConfigurationBuilder().AddJsonFile("appSettings.json").Build();
string HashKey = Config.GetSection("HashKey").Value;//API 串接金鑰
string HashIV = Config.GetSection("HashIV").Value;//API 串接密碼
string TradeInfoDecrypt = DecryptAESHex(Request.Form["TradeInfo"], HashKey, HashIV);
NameValueCollection decryptTradeCollection = HttpUtility.ParseQueryString(TradeInfoDecrypt);
receive.Length = 0;
foreach (String key in decryptTradeCollection.AllKeys)
receive.AppendLine(key + "=" + decryptTradeCollection[key] + "<br>");
ViewData["TradeInfo"] = receive.ToString();
return View();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/// <summary>
/// 商店取號網址
/// </summary>
/// <returns></returns>
public
IActionResult
CallbackCustomer
(
)
{
// 接收參數
StringBuilder
receive
=
new
StringBuilder
(
)
;
foreach
(
var
item
in
Request
.
Form
)
{
receive
.
AppendLine
(
item
.
Key
+
"="
+
item
.
Value
+
"<br>"
)
;
}
ViewData
[
"ReceiveObj"
]
=
receive
.
ToString
(
)
;
// 解密訊息
IConfiguration
Config
=
new
ConfigurationBuilder
(
)
.
AddJsonFile
(
"appSettings.json"
)
.
Build
(
)
;
string
HashKey
=
Config
.
GetSection
(
"HashKey"
)
.
Value
;
//API 串接金鑰
string
HashIV
=
Config
.
GetSection
(
"HashIV"
)
.
Value
;
//API 串接密碼
string
TradeInfoDecrypt
=
DecryptAESHex
(
Request
.
Form
[
"TradeInfo"
]
,
HashKey
,
HashIV
)
;
NameValueCollection
decryptTradeCollection
=
HttpUtility
.
ParseQueryString
(
TradeInfoDecrypt
)
;
receive
.
Length
=
0
;
foreach
(
String
key
in
decryptTradeCollection
.
AllKeys
)
{
receive
.
AppendLine
(
key
+
"="
+
decryptTradeCollection
[
key
]
+
"<br>"
)
;
}
ViewData
[
"TradeInfo"
]
=
receive
.
ToString
(
)
;
return
View
(
)
;
}
我們可以從 BankCode 和 CodeNo 知道使用者要匯款的銀行代碼和帳號。
加入支付通知網址方法
這個方法是當使用者實際付款完成時,不管是信用卡、ATM 或超商付款,會收到的通知,這個方法不需要設計顯示頁面。
例如使用者選擇 ATM 付款時,任何時間在 ATM 完成付款,我們從這方法收到通知後,寫入資料庫內紀錄使用者已付款就好。
同樣在 HomeController 內加入此語法:
foreach (var item in Request.Form)
receive.AppendLine(item.Key + "=" + item.Value + "<br>");
ViewData["ReceiveObj"] = receive.ToString();
// 解密訊息
IConfiguration Config = new ConfigurationBuilder().AddJsonFile("appSettings.json").Build();
string HashKey = Config.GetSection("HashKey").Value;//API 串接金鑰
string HashIV = Config.GetSection("HashIV").Value;//API 串接密碼
string TradeInfoDecrypt = DecryptAESHex(Request.Form["TradeInfo"], HashKey, HashIV);
NameValueCollection decryptTradeCollection = HttpUtility.ParseQueryString(TradeInfoDecrypt);
receive.Length = 0;
foreach (String key in decryptTradeCollection.AllKeys)
receive.AppendLine(key + "=" + decryptTradeCollection[key] + "<br>");
ViewData["TradeInfo"] = receive.ToString();
return View();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/// <summary>
/// 支付通知網址
/// </summary>
/// <returns></returns>
public
IActionResult
CallbackNotify
(
)
{
// 接收參數
StringBuilder
receive
=
new
StringBuilder
(
)
;
foreach
(
var
item
in
Request
.
Form
)
{
receive
.
AppendLine
(
item
.
Key
+
"="
+
item
.
Value
+
"<br>"
)
;
}
ViewData
[
"ReceiveObj"
]
=
receive
.
ToString
(
)
;
// 解密訊息
IConfiguration
Config
=
new
ConfigurationBuilder
(
)
.
AddJsonFile
(
"appSettings.json"
)
.
Build
(
)
;
string
HashKey
=
Config
.
GetSection
(
"HashKey"
)
.
Value
;
//API 串接金鑰
string
HashIV
=
Config
.
GetSection
(
"HashIV"
)
.
Value
;
//API 串接密碼
string
TradeInfoDecrypt
=
DecryptAESHex
(
Request
.
Form
[
"TradeInfo"
]
,
HashKey
,
HashIV
)
;
NameValueCollection
decryptTradeCollection
=
HttpUtility
.
ParseQueryString
(
TradeInfoDecrypt
)
;
receive
.
Length
=
0
;
foreach
(
String
key
in
decryptTradeCollection
.
AllKeys
)
{
receive
.
AppendLine
(
key
+
"="
+
decryptTradeCollection
[
key
]
+
"<br>"
)
;
}
ViewData
[
"TradeInfo"
]
=
receive
.
ToString
(
)
;
return
View
(
)
;
}
其實這 3 個回傳接收方法解密方式都一樣,也使用相同的 API 金鑰,可以將 API 金鑰放在設定檔內保存。
然後我示範解密的方法,實際上再依各自的需求修改讀取需要的資料。
再次提醒一下,你要測試藍新金流時,首先要在測試環境下串接,測試完整後,再到正式環境再測一遍,然後要有對外網域或 IP 才可以測試,最後祝大家串接順利。
連結 GitHub 下載範例
相關學習文章
[WordPress] 如何在 WooCommerce 新增 PayNow 立吉富線上金流
如果你在學習上有不懂的地方,需要諮詢服務,可以參考
站長服務
,我想辨法解決你的問題
如果文章內容有過時、不適用或錯誤的地方,幫我在下方留言通知我一下,謝謝
舊版 Office 2010 – 免破解,到期後可繼續使用
- 100,617 views
Windows Server 如何安裝 SQL Server 2019 免費開發版
- 75,890 views
如何將亂碼簡體檔名、資料夾在繁體電腦正常顯示-使用 ConvertZZ 免費軟體
- 75,863 views
免費 FTP 伺服器 FileZilla Server 安裝教學 (新版設定)
- 42,813 views
如何設定 Windows 開機自動登入帳號密碼
- 38,813 views
如何申請免費 Let’s Encrypt SSL 自動更新憑證,自架 IIS 站台適用
- 38,046 views
Windows Server 2019 如何安裝 IIS 運行 ASP.NET 專案
- 29,330 views
[開箱] ASUS E510MA 夢幻白 15.6 吋平價文書筆電開箱評價
- 28,769 views
Windows Server 安裝 MySQL Community 免費社群版
- 28,106 views
[C# WinForm] 如何讓 ComboBox 也能設定值 (Value) 與名稱 (Text)
- 27,281 views
如何在 ASP.NET Core MVC 加入 Area 區域
玉山信用卡智能客服剪卡(停卡、註銷) 操作
Google Chrome 遠端桌面如何支援存取別人的電腦
WordPress SEO 優化教學,掌握大小方向操作技巧
練習就可以改變個性,就像你一直練習成為厲害工程師一樣
提供廣告版位出租、聯盟行銷及業配文章合作等內容,有需要商業合作的廠商請與我聯絡。