在 ASP.NET 中使用 PayPal 支付系统






4.86/5 (155投票s)
本文介绍了在 ASP.NET 中使用 PayPal 支付系统的各个方面

引言
那些创建商业网站的人都会面临一个问题:“它应该如何收款?”世界上最受欢迎的支付系统之一是 PayPal。选择此系统通常是因为它可靠、易于使用且易于开立账户。要开立账户,您只需要一张信用卡和/或一个美国银行账户。该系统的缺点之一是其严格的安全策略。然而,实践表明,如果您仔细遵守系统规则,那么错误是非常罕见的。本文的目的是展示如何组织支付处理以支持可靠性和安全性。本文还旨在为您提供一个简单在线商店的开发示例,以演示与 PayPal 系统的交互。您可以在您的应用程序中使用代码来组织与 PayPal 系统的交互和处理支付。
本文特别关注使用 IPN(即时支付通知)自动验证支付的过程。本文基于 KB_Soft Group 的经验和 PayPal 官方文档。
PayPal 支付类型
PayPal 支持多种支付类型
- PayPal 购物车中的商品支付。在这种情况下,PayPal 负责支持购物车的所有操作。不幸的是,此选项无法提供实现某些项目所需的最大灵活性,因此本文不考虑此选项。
- “一键”购物。在这种情况下,商品不会放入购物车。此方法也用于支付在没有 PayPal 的情况下填充的购物车中的商品。这就是为什么此选项提供最大灵活性和对购物车的完全控制。
- 定期账单或订阅。PayPal 提供订阅功能,这意味着一笔确定的金额将定期从用户的账户转账到卖家的账户。用户可以随时取消订阅。卖家可以指定订阅的期限和费用。他还可以组织一个试用期,让用户评估他所提供服务的质量。试用期可以是付费或免费的。
由于上述原因,本文将考虑第二种选择。为了简化示例,订阅将不予描述。为了与 PayPal 系统交互,KB_Soft Group 使用 UserControl,这是为此目的内部开发的产品。给定的与 PayPal 配合使用的代码示例使用特殊的 HTML 表单进行请求,以便清楚地解释与系统的交互。PayPal 也将其自己的控件作为动态库提供,但遗憾的是,此控件仅在美式区域设置 (en-us) 下才能正常运行。此外,它不提供与 PayPal 配合使用的完整功能。它也不提供在某些项目上工作所需的灵活性。
支付过程
支付过程非常简单。创建了一个带有隐藏字段集的 POST 表单,其中包含商品信息(标识符、名称和成本)以及一个发送表单的按钮。需要注意的是,所有价格都应以小数点后两位表示。如果商品价格为 10 美元,则其价格应表示为“10.00”。发送表单后,买家将前往 PayPal 网站并完成支付过程。使用真实的 PayPal 账户时,表单应发送到 此处。开发的示例还允许您使用 PayPal 沙盒。使用 web.config 的 UseSandbox
参数,表单将发送到 此处。
“一键”购物
最简单表单的代码
<form method="post" action= "https://www.paypal.com/cgi-bin/webscr">
<input type="hidden" name="cmd" value="_xclick">
<input type="hidden" name="business" value="my@email.com">
<input type="hidden" name="item_name" value="Item name">
<input type="hidden" name="item_number" value="1234">
<input type="hidden" name="amount" value="19.95">
<input type="hidden" name="no_shipping" value="1">
<input type="submit" value="Buy Now">
</form>
主要参数说明
参数 | 描述 |
cmd |
该参数是强制性的。对于未加密请求,它必须具有 _xclick 值。 |
business |
该参数是强制性的,表示卖家的电子邮件。 |
item_number |
此参数是商品标识符。此值不会显示给用户;但是,它将在交易确认时传递给您的脚本。如果您计划使用 PayPal 支付购物车中的商品,则可以在此参数中传递购物车的标识符。 |
item_name |
这是将显示给用户的商品名称。 |
no_shipping |
提示客户输入收货地址。 默认或 0:提示客户包含收货地址。 1:不要求客户提供收货地址。 2:客户必须提供收货地址。 |
return |
这是用户成功支付后将被重定向到的 URL。如果未传递此参数,买家将留在 PayPal 网站上。 |
rm |
此参数确定有关成功交易的信息将传递给 return 参数中指定的脚本的方式。“1”表示不传递任何参数。“2”表示将使用 POST 方法。“0”表示将使用 GET 方法。该参数默认为“0”。 |
cancel_return |
这是用户取消支付时将被重定向到的 URL。如果未传递该参数,买家将留在 PayPal 网站上。 |
notify_url |
这是 PayPal 将传递交易信息 (IPN) 的 URL。如果未传递该参数,将使用账户设置中的值。如果账户设置中未定义此值,则将不使用 IPN。 |
custom |
此字段不参与购物过程,它将在交易确认时简单地传递给 IPN 脚本。 |
invoice |
此参数用于传递发票号。该参数不是强制性的,但如果传递,则对于每次交易都必须是唯一的。 |
amount |
此参数表示支付金额。如果未传递该参数,将允许用户输入金额(这用于捐赠)。 |
currency_code |
此参数表示货币代码。可能的值为“USD ”、“EUR ”、“GBP ”、“YEN ”、“CAD ”等。默认为“USD ”。 |
表 1 列出了最常用的参数。有关参数的完整列表,请参阅文章末尾参考文献中提供的 PayPal 文档。
IPN
IPN(即时支付通知)是 PayPal 的一项技术,允许支付处理自动化。该技术的本质在于在卖方服务器上创建的特殊脚本。当发生与卖方账户相关的事件时——例如,支付转账、支付取消、订阅创建或取消等——PayPal 服务器会向 IPN 脚本发送一个带有交易信息的 POST
请求。该脚本反过来会向 PayPal 服务器发送请求以验证交易。
因此,买家已完成支付。在最多几秒钟的延迟后,PayPal 服务器会向账户设置中指定或在 notify_url
参数中传递的 IPN 脚本发送请求。一个好的 IPN 脚本是支付安全的关键。如果您曾听说使用 PayPal 的卖家是欺诈行为的受害者,那么请确信这些卖家要么根本不使用 IPN,要么拥有一个糟糕的 IPN 脚本。
首先,脚本必须确保它是由 PayPal 服务器调用的。为此,脚本会向 此处 或 此处 生成一个 POST
请求。它将收到的所有变量原样传递,以及 cmd
参数,其值为 _notify-validate
。作为对此请求的响应,脚本会收到 VERIFIED
(表示交易已成功验证)或 INVALID
(表示发生错误)。如果脚本收到 INVALID
,则必须终止。
然后,脚本必须检查收款人,因为潜在的入侵者可能会更改支付表单,使其发送到他自己的账户。收款人由 business
和 receiver_email
变量确定。需要两个变量,因为 PayPal 允许为一个账户注册多个电子邮件。创建账户时指定的电子邮件是主电子邮件。receiver_email
始终是主电子邮件。如果支付发送到另一个附加电子邮件,则该电子邮件会在 business
参数中传递。如果 business
和/或 receiver_email
不包含预期值,则脚本会立即终止。
然后脚本必须检查支付金额和货币。此验证是必需的,因为潜在的入侵者可能会更改表单中的支付金额。如果使用订阅,脚本应检查所有订阅参数,即设置了哪些参数,试用期的持续时间和成本,主订阅周期的持续时间和成本等。
同一交易的 IPN 可以发送不止一次。例如,如果支付因某种原因延迟,第一次 IPN 将在支付后立即发送。支付完成或取消后,第二次 IPN 会发送。如果 IPN 脚本不返回等于 200 的 HTTP 状态,则 PayPal 会在一段时间后再次发送 IPN。第一次会在 10 秒后重复,如果需要,则在 20 秒后重复,然后是 40 秒、80 秒等,最长可达 24 小时。如果脚本在 4 天内没有响应,则 PayPal 会停止发送 IPN。这可以用于在 IPN 脚本发生错误时不会丢失交易信息。例如,如果脚本无法连接到存储交易信息的数据库,则脚本可以返回 HTTP 状态 500,IPN 将在稍后重复。如果 IPN 脚本不引用 PayPal 服务器来验证交易,则重复的 IPN 将以相同的方式发送。
从 return
、rm
和 notify_url
参数的描述中可以看出,IPN 可以传递给 return
和 notify_url
参数中指定的两个脚本。它们之间有两个区别:
return
的 IPN 只会在支付执行后发送一次。notify_url
可以多次调用(参见上一段)。return
脚本的结果将显示给用户。需要注意的是,如果结果包含链接,则链接必须是绝对链接。notify_url
脚本的结果不会在浏览器中显示。
收到的 POST
变量包含交易信息。最常用的变量如下:
参数 | 描述 |
txn_id |
唯一的交易编号。 |
payment_date |
支付日期,格式为“18:30:30 Jan 1, 2000 PST”。 |
payer_email |
买家电子邮件。 |
business |
卖家电子邮件。 |
payer_id |
买家的唯一标识符。通过 PayPal 进行支付的参与者通过电子邮件地址进行识别。但是,考虑到电子邮件可能更改,最好使用 payer_id 来识别买家。 |
item_number |
商品标识符。 |
item_name |
商品名称。 |
txn_type |
交易类型。可能的值为:web_accept - 支付是通过点击“立即购买”按钮执行的。cart - 支付是使用内置的 PayPal 购物车执行的。send_money - 支付是使用“发送资金”功能执行的。reversal - 资金在买方主动下退回给买方。 |
payment_status |
付款状态。可能的值为:Completed - 交易成功完成,资金已转入卖方账户。如果 txn_type =reversal ,则资金已退回买方账户。Pending - 支付已延迟。延迟原因在 pending_reason 变量中确定。支付完成后,PayPal 将发送另一个通知。Failed - 支付失败。此状态仅在通过银行账户进行支付时可能出现。Denied - 卖家取消了支付。当卖家取消处于 Pending 状态的支付时,支付处于此状态。Refunded - 资金已退还给买家。当卖家取消处于 Completed 状态的支付时,支付处于此状态。 |
pending_reason |
付款延迟原因。可能的值有:echeck - 通过电子支票支付multi_currency - 支付使用的货币是卖家账户设置中指定的货币。支付将在卖家确认交易后完成。intl - 卖家不是美国居民。支付将在卖家确认交易后完成。verify - 卖家账户处于 unverified 状态。支付将在卖家身份验证后完成。address - 卖家账户设置要求买家指定收货地址,但买家未指定地址。支付将在卖家确认交易后完成。upgrade - 支付是使用信用卡进行的,卖家账户状态为 Personal 。要完成支付,卖家应将账户升级为 Business 或 Premier 。unilateral - 卖家的电子邮件未在系统中注册。other - 其他原因。卖家需要联系支持人员以了解更多原因。 |
payment_type |
付款类型。可能的值为:echeck - 支付是通过电子支票完成的。instant - 支付是通过信用卡、银行账户或买家 PayPal 账户中的资金完成的。 |
mc_gross |
支付金额。 |
mc_fee |
佣金费用。存入卖家账户的金额计算为 mc_gross – mc_fee 。 |
mc_currency |
支付货币。 |
first_name |
买家姓名。 |
last_name |
买家姓氏。 |
address_street |
街道。 |
address_city |
城市。 |
address_state |
州/省。 |
address_zip |
邮政编码。 |
address_country |
国家。 |
verify_sign |
数字签名。在 PayPal 中用于交易验证。 |
IPN 处理示例
下面是使用 PayPal IPN 的脚本示例。我们发布此脚本并非为了向您提供一个可以直接复制/粘贴的脚本,而是为了说明 IPN 工作的一般原理。KB_Soft Group 在使用 PayPal 系统创建网站时使用更复杂的脚本。此脚本相对简单,但同时它也说明了 IPN 处理的主要原理。
所提供的代码创建了一个简化版的在线商店。买家将商品添加到购物车并支付。买家支付后,会生成一份支付报告。
所有关于商品和购物车内容的信息都存储在 XML 文件中。我们选择这种信息存储方式只是出于兼容性原因。任何下载代码的用户都可以轻松调整和测试所创建的在线商店。至于实际应用程序,最好使用数据库来存储有关商品、购物车、支付请求及其响应的信息。
为了存储商品信息,我们使用 Goods.xml 文件,其结构如下:
<Goods>
<Good id="0" name="Sample of good" price="10.99" />
</Goods>
其中...
id
是唯一的商品标识符name
是商品名称price
是商品价格
为了简化本示例,我们没有为所创建的在线商店提供允许将新商品添加到商品目录的功能。如有需要,可以将新商品的信息手动添加到 XML 文件中。
为了存储购物车信息,我们使用 Carts.xml 文件,其结构如下:
<Carts>
<Cart rec_id="0" cart_id="1" item_id="0" price="10.99" quantity="1" />
</Carts>
其中...
rec_id
是唯一的记录标识符cart_id
是包含此商品的购物车的标识符id
是商品标识符price
是商品价格quantity
是订购商品的数量
对于真实的在线商店,已支付购物车的信息不会存储在那里,而是写入数据库的订单表中。为了简化过程并让您轻松跟踪支付结果,我们没有在给定代码中实现此功能。此外,真实的在线商店应注册用户,以便能够识别他们并创建只有其用户才能访问的购物车。给定示例不使用注册,因此它不控制不同用户对购物车的访问。此过程已简化,因为用户只需选择其购物车标识符即可。
为了存储支付请求信息,我们使用 PaymentRequests.xml 文件,其结构如下:
<Requests>
<Request request_id="0" cart_id="1" price="10.99"
request_date="5/28/2007 1:15:18 PM" />
</Requests>
其中...
request_id
是唯一的请求标识符cart_id
是正在支付的购物车的标识符price
是商品成本request_date
是请求创建的日期和时间
在真实的在线商店中,支付请求信息包含支付详情表中的支付详情标识符。为了简化代码,我们没有使用此功能。
为了存储支付请求的响应信息,我们使用 PaymentResponses.xml 文件,其结构如下:
<Responses>
<Response payment_id="0"
txn_id="3PP58082BD3079037"
payment_date="5/28/2007 1:22:40 PM"
payment_price="10.99"
email= my@email.com
first_name=""
last_name=""
street=""
city=""
state=""
zip=""
country=""
request_id="0"
is_success="True"
reason_fault=""
/>
</Responses>
其中...
payment_id
是唯一的支付标识符txn_id
是 PayPal 交易的唯一编号payment_date
是执行支付的日期和时间payment_price
是支付金额email
是买家的电子邮件first_name
是买家的名字last_name
是买家的姓氏street
是买家的街道city
是买家的城市state
是买家的州zip
是买家的邮政编码country
是买家的国家request_id
是支付请求的标识符is_success
指示支付是否成功执行reason_fault
是支付失败的可能原因
如果使用 XML 文件存储信息,最好使用 XML 架构来验证信息。但是,为了简化示例,我们不执行验证。
发送到 PayPal 的支付请求表单如下:
<form id="payForm" method="post" action="<%Response.Write (URL)%>">
<input type="hidden" name="cmd" value="<%Response.Write (cmd)%>">
<input type="hidden" name="business"
value="<%Response.Write (business)%>">
<input type="hidden" name="item_name"
value="<%Response.Write (item_name)%>">
<input type="hidden" name="amount" value="<%Response.Write (amount)%>">
<input type="hidden" name="no_shipping"
value="<%Response.Write (no_shipping)%>">
<input type="hidden" name="return"
value="<%Response.Write (return_url)%>">
<input type="hidden" name="rm" value="<%Response.Write (rm)%>">
<input type="hidden" name="notify_url"
value="<%Response.Write (notify_url)%>">
<input type="hidden" name="cancel_return"
value="<%Response.Write (cancel_url)%>">
<input type="hidden" name="currency_code"
value="<%Response.Write (currency_code)%>">
<input type="hidden" name="custom"
value="<%Response.Write (request_id)%>">
</form>
其中...
URL
是用于操作的 URL,取决于应使用沙盒还是真实的 PayPal 账户cmd
是发送到 PayPal 的命令business
是卖家的电子邮件item_name
是商品名称——即买家支付的商品——将显示给用户;amount
是支付金额no_shipping
是一个参数,用于确定是否应请求配送地址return_url
是支付成功后买家将被重定向到的 URLrm
是一个参数,用于确定有关成功完成的交易的信息将发送到 return 参数中指定的脚本的方式notify_url
是 PayPal 将发送交易信息 (IPN) 的 URLcancel_url
是买家取消支付时将被重定向到的 URLcurrency_code
是货币代码request_id
是支付请求的标识符
变量的值在本文附带的源代码的 PayPal.aspx.cs 或 tPayPal.aspx.vb 文件中设置。有关表单字段的更详细描述,请参见表 1。
当在 custom
字段中传递 request_id
时,它允许 IPN 脚本恢复购物车信息。如果买家取消支付,他将被重定向到 cancel_url
。但是,如果他执行支付,他将被重定向到 return_url
。在后一种情况下,我们可以测试与 PayPal 的交互,检查支付是否已执行,创建支付报告并感谢买家的购买。对于给定的示例,出于安全目的,请仅将 payment_success.aspx.cs 或 payment_success.aspx.vb 文件中的 IPN 处理代码用于测试,因为真实产品应在 notify_url
参数中指定的 IPN 脚本中验证支付。payment_success.aspx.cs 或 payment_success.aspx.vb 代码是专门编写的,旨在使测试过程提供尽可能多的信息。该代码包含仅在测试阶段重要的消息。此信息写入日志文件。该文件不仅存储关键错误,还存储允许网站继续运行的错误。
通常,错误消息应妥善处理,而不是以异常形式显示给用户。Response.Write()
结构也不是一个好主意。实际网站通常会创建一个特殊页面,用于发送错误信息。然后对信息进行格式化并显示给用户。例如,如果抛出异常或请求的页面不存在,用户应被重定向到此页面。为了简化给定的示例,代码将大部分发生的错误信息写入日志文件。
返回参数很有用,因为当支付执行时,它允许将验证结果显示给用户。但是,验证并不能 100% 保证支付确实已存入卖家账户。例如,如果买家使用电子支票,那么只有在银行处理支票后,支付才会存入卖家账户,这也不能保证资金已存入账户。这就是为什么真实的在线商店应该使用 IPN 并处理支付,检查支付并在 IPN 脚本的代码中进行协议。此外,在发送表单内容之前应将其加密,以避免伪造支付信息。这意味着应使用所谓的加密网站支付。如果您不打算使用加密网站支付 (EWP) 验证,则必须检查 IPN 发送给您的价格、交易 ID、PayPal 收款人电子邮件地址和其他数据,以确保它们正确无误。通过检查数据,您可以确保您没有受到欺骗。
KB_Soft Group 在其项目中同时使用 EWP 和来自 PayPal 的参数验证。这提供了重复的验证检查,并排除了任何信息伪造的可能性。为了简化给定的在线商店示例并使其适用于任何 PayPal 账户,我们仅使用 IPN。这是因为如果使用 EWP,我们必须创建私钥和公钥,并将创建的公钥上传到我们在 PayPal 服务器上的账户。然后我们需要使用获得的证书标识符来加密请求表单。此外,要使用 EWP,我们需要下载 PayPal 系统本身的公钥。本文不旨在详细描述 EPW 的工作原理,因此您可以访问 PayPal 网站以查找有关此问题的详细信息。
IPNHandler
类的 Page_Load
过程代码如下所示。您可以在本文附带的源代码档案中找到详细信息。
C#
private void Page_Load(object sender, EventArgs e)
{
string requestUriString;
CultureInfo provider = new CultureInfo("en-us");
string requestsFile = this.Server.MapPath(
"~/App_Data/PaymentRequests.xml");
requests.Clear();
if (System.IO.File.Exists(requestsFile))
{
requests.ReadXml(requestsFile);
}
else
{
Carts.CreateXml(requestsFile, "Requests");
requests.ReadXml(requestsFile);
}
string responseFile = this.Server.MapPath(
"~/App_Data/PaymentResponses.xml");
responses.Clear();
if (System.IO.File.Exists(responseFile))
{
responses.ReadXml(responseFile);
}
else
{
Carts.CreateXml(responseFile, "Responses");
responses.ReadXml(responseFile);
}
string strFormValues = Encoding.ASCII.GetString(
this.Request.BinaryRead(this.Request.ContentLength));
// getting the URL to work with
if (String.Compare(
ConfigurationManager.AppSettings["UseSandbox"].ToString(),
"true", false) == 0)
{
requestUriString =
"https://www.sandbox.paypal.com/cgi-bin/webscr";
}
else
{
requestUriString = "https://www.paypal.com/cgi-bin/webscr";
}
// Create the request back
HttpWebRequest request =
(HttpWebRequest)WebRequest.Create(requestUriString);
// Set values for the request back
request.Method = "POST";
request.ContentType = "application/x-www-form-urlencoded";
string obj2 = strFormValues + "&cmd=_notify-validate";
request.ContentLength = obj2.Length;
// Write the request back IPN strings
StreamWriter writer =
new StreamWriter(request.GetRequestStream(), Encoding.ASCII);
writer.Write(RuntimeHelpers.GetObjectValue(obj2));
writer.Close();
//send the request, read the response
HttpWebResponse response = (HttpWebResponse)request.GetResponse();
Stream responseStream = response.GetResponseStream();
Encoding encoding = Encoding.GetEncoding("utf-8");
StreamReader reader = new StreamReader(responseStream, encoding);
// Reads 256 characters at a time.
char[] buffer = new char[0x101];
int length = reader.Read(buffer, 0, 0x100);
while (length > 0)
{
// Dumps the 256 characters to a string
string requestPrice;
string IPNResponse = new string(buffer, 0, length);
length = reader.Read(buffer, 0, 0x100);
try
{
// getting the total cost of the goods in
// cart for an identifier
// of the request stored in the "custom" variable
requestPrice =
GetRequestPrice(this.Request["custom"].ToString());
if (String.Compare(requestPrice, "", false) == 0)
{
Carts.WriteFile("Error in IPNHandler: amount = \");
reader.Close();
response.Close();
return;
}
}
catch (Exception exception)
{
Carts.WriteFile("Error in IPNHandler: " + exception.Message);
reader.Close();
response.Close();
return;
}
NumberFormatInfo info2 = new NumberFormatInfo();
info2.NumberDecimalSeparator = ".";
info2.NumberGroupSeparator = ",";
info2.NumberGroupSizes = new int[] { 3 };
// if the request is verified
if (String.Compare(IPNResponse, "VERIFIED", false) == 0)
{
// check the receiver's e-mail (login is user's
// identifier in PayPal)
// and the transaction type
if ((String.Compare(this.Request["receiver_email"],
this.business, false) != 0) ||
(String.Compare(this.Request["txn_type"],
"web_accept", false) != 0))
{
try
{
// parameters are not correct. Write a
// response from PayPal
// and create a record in the Log file.
this.CreatePaymentResponses(this.Request["txn_id"],
Convert.ToDecimal(
this.Request["mc_gross"], info2),
this.Request["payer_email"],
this.Request["first_name"],
this.Request["last_name"],
this.Request["address_street"],
this.Request["address_city"],
this.Request["address_state"],
this.Request["address_zip"],
this.Request["address_country"],
Convert.ToInt32(this.Request["custom"]), false,
"INVALID payment's parameters" +
"(receiver_email or txn_type)");
Carts.WriteFile(
"Error in IPNHandler: INVALID payment's" +
" parameters(receiver_email or txn_type)");
}
catch (Exception exception)
{
Carts.WriteFile("Error in IPNHandler: " +
exception.Message);
}
reader.Close();
response.Close();
return;
}
// check whether this request was performed
// earlier for its identifier
if (this.IsDuplicateID(this.Request["txn_id"]))
{
// the current request is processed. Write
// a response from PayPal
// and create a record in the Log file.
this.CreatePaymentResponses(this.Request["txn_id"],
Convert.ToDecimal(this.Request["mc_gross"], info2),
this.Request["payer_email"],
this.Request["first_name"],
this.Request["last_name"],
this.Request["address_street"],
this.Request["address_city"],
this.Request["address_state"],
this.Request["address_zip"],
this.Request["address_country"],
Convert.ToInt32(this.Request["custom"]), false,
"Duplicate txn_id found");
Carts.WriteFile(
"Error in IPNHandler: Duplicate txn_id found");
reader.Close();
response.Close();
return;
}
// the amount of payment, the status of the
// payment, and a possible reason of delay
// The fact that Getting txn_type=web_accept or
// txn_type=subscr_payment are got odes not mean that
// seller will receive the payment.
// That's why we check payment_status=completed. The
// single exception is when the seller's account in
// not American and pending_reason=intl
if (((String.Compare(
this.Request["mc_gross"].ToString(provider),
requestPrice, false) != 0) ||
(String.Compare(this.Request["mc_currency"],
this.currency_code, false) != 0)) ||
((String.Compare(this.Request["payment_status"],
"Completed", false) != 0) &&
(String.Compare(this.Request["pending_reason"],
"intl", false) != 0)))
{
// parameters are incorrect or the payment
// was delayed. A response from PayPal should not be
// written to DB of an XML file
// because it may lead to a failure of
// uniqueness check of the request identifier.
// Create a record in the Log file with information
// about the request.
Carts.WriteFile(
"Error in IPNHandler: INVALID payment's parameters."+
"Request: " + strFormValues);
reader.Close();
response.Close();
return;
}
try
{
// write a response from PayPal
this.CreatePaymentResponses(this.Request["txn_id"],
Convert.ToDecimal(this.Request["mc_gross"], info2),
this.Request["payer_email"],
this.Request["first_name"],
this.Request["last_name"],
this.Request["address_street"],
this.Request["address_city"],
this.Request["address_state"],
this.Request["address_zip"],
this.Request["address_country"],
Convert.ToInt32(this.Request["custom"]), true, "");
Carts.WriteFile(
"Success in IPNHandler: PaymentResponses created");
///////////////////////////////////////////////////
// Here we notify the person responsible for
// goods delivery that
// the payment was performed and providing
// him with all needed information about
// the payment. Some flags informing that
// user paid for a services can be also set here.
// For example, if user paid for registration
// on the site, then the flag should be set
// allowing the user who paid to access the site
//////////////////////////////////////////////////
}
catch (Exception exception)
{
Carts.WriteFile(
"Error in IPNHandler: " + exception.Message);
}
}
else
{
Carts.WriteFile(
"Error in IPNHandler. IPNResponse = 'INVALID'");
}
}
reader.Close();
response.Close();
}
Visual Basic
Private Sub Page_Load(ByVal sender As System.Object,
ByVal e As System.EventArgs) Handles MyBase.Load
Dim ci As CultureInfo = New CultureInfo("en-us")
Dim requestsFile As String = Server.MapPath(
"~/App_Data/PaymentRequests.xml")
requests.Clear()
If File.Exists(requestsFile) Then
requests.ReadXml(requestsFile)
Else
KBSoft.Carts.CreateXml(requestsFile, "Requests")
requests.ReadXml(requestsFile)
End If
Dim responseFile As String = Server.MapPath(
"~/App_Data/PaymentResponses.xml")
responses.Clear()
If File.Exists(responseFile) Then
responses.ReadXml(responseFile)
Else
KBSoft.Carts.CreateXml(responseFile, "Responses")
responses.ReadXml(responseFile)
End If
Dim strFormValues As String = Encoding.ASCII.GetString(
Request.BinaryRead(Request.ContentLength))
Dim strNewValue
' getting the URL to work with
Dim URL As String
If AppSettings("UseSandbox").ToString = "true" Then
URL = "https://www.sandbox.paypal.com/cgi-bin/webscr"
Else
URL = "https://www.paypal.com/cgi-bin/webscr"
End If
' Create the request back
Dim req As HttpWebRequest = CType(WebRequest.Create(URL),
HttpWebRequest)
' Set values for the request back
req.Method = "POST"
req.ContentType = "application/x-www-form-urlencoded"
strNewValue = strFormValues + "&cmd=_notify-validate"
req.ContentLength = strNewValue.Length
' Write the request back IPN strings
Dim stOut As StreamWriter = New StreamWriter(
req.GetRequestStream(), _
Encoding.ASCII)
stOut.Write(strNewValue)
stOut.Close()
'send the request, read the response
Dim strResponse As HttpWebResponse = CType(req.GetResponse(),
HttpWebResponse)
Dim IPNResponseStream As Stream = strResponse.GetResponseStream
Dim encode As Encoding = System.Text.Encoding.GetEncoding("utf-8")
Dim readStream As New StreamReader(IPNResponseStream, encode)
Dim read(256) As [Char]
' Reads 256 characters at a time.
Dim count As Integer = readStream.Read(read, 0, 256)
While count > 0
' Dumps the 256 characters to a string
Dim IPNResponse As New [String](read, 0, count)
count = readStream.Read(read, 0, 256)
Dim amount As String
Try
' getting the total cost of the goods in cart for an
' identifier of the request stored in the "custom"
' variable
amount = GetRequestPrice(Request("custom").ToString)
If amount = "" Then
KBSoft.Carts.WriteFile("Error in IPNHandler: amount = """)
readStream.Close()
strResponse.Close()
Return
End If
Catch ex As Exception
KBSoft.Carts.WriteFile("Error in IPNHandler: " + ex.Message)
readStream.Close()
strResponse.Close()
Return
End Try
Dim provider As NumberFormatInfo = New NumberFormatInfo()
provider.NumberDecimalSeparator = "."
provider.NumberGroupSeparator = ","
provider.NumberGroupSizes = New Integer() {3}
' if the request is verified
If IPNResponse = "VERIFIED" Then
' check the receiver's e-mail (login is user's
' identifier in PayPal) and the transaction type
If Request("receiver_email") <> business Or Request(
"txn_type") <> "web_accept" Then
Try
' parameters are not correct. Write a response from
' PayPal and create a record in the Log file.
CreatePaymentResponses(Request("txn_id"),
Convert.ToDecimal(Request("mc_gross"), provider), _
Request("payer_email"), Request("first_name"),
Request("last_name"), Request("address_street"), _
Request("address_city"), Request("address_state"),
Request("address_zip"), Request("address_country"), _
Convert.ToInt32(Request("custom")), False,
"INVALID payment's parameters (
receiver_email or txn_type)")
KBSoft.Carts.WriteFile(
"Error in IPNHandler: INVALID payment's parameters"+
" (receiver_email or txn_type)")
Catch ex As Exception
KBSoft.Carts.WriteFile(
"Error in IPNHandler: " + ex.Message)
End Try
readStream.Close()
strResponse.Close()
Return
End If
' check whether this request was performed earlier for its
' identifier
If IsDuplicateID(Request("txn_id")) Then
' the current request is processed. Write a response from
' PayPal and create a record in the Log file.
CreatePaymentResponses(Request("txn_id"),
Convert.ToDecimal(Request("mc_gross"), provider), _
Request("payer_email"), Request("first_name"),
Request("last_name"), Request("address_street"), _
Request("address_city"), Request("address_state"),
Request("address_zip"), Request("address_country"), _
Convert.ToInt32(Request("custom")), False,
"Duplicate txn_id found")
KBSoft.Carts.WriteFile(
"Error in IPNHandler: Duplicate txn_id found")
readStream.Close()
strResponse.Close()
Return
End If
' the amount of payment, the status of the payment, and a
' possible reason of delay
' The fact that Getting txn_type=web_accept or
' txn_type=subscr_payment are got odes not mean that
' seller will receive the payment.
' That's why we check payment_status=completed. The
' single exception is when the seller's account in
' not American and pending_reason=intl
If Request("mc_gross").ToString(ci) <> amount Or Request(
"mc_currency") <> currency_code Or _
(Request("payment_status") <> "Completed" And Request(
"pending_reason") <> "intl") Then
' parameters are incorrect or the payment was delayed.
' A response from PayPal should not be
' written to DB of an XML file
' because it may lead to a failure of uniqueness check of
' the request identifier.
' Create a record in the Log file with information about
' the request.
KBSoft.Carts.WriteFile(
"Error in IPNHandler: INVALID payment's parameters."+
" Request: " + strFormValues)
readStream.Close()
strResponse.Close()
Return
End If
Try
' write a response from PayPal
CreatePaymentResponses(Request("txn_id"),
Convert.ToDecimal(Request("mc_gross"), provider), _
Request("payer_email"), Request("first_name"),
Request("last_name"), Request("address_street"), _
Request("address_city"), Request("address_state"),
Request("address_zip"), Request("address_country"), _
Convert.ToInt32(Request("custom")), True, "")
KBSoft.Carts.WriteFile(
"Success in IPNHandler: PaymentResponses created")
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Here we notify the person responsible for goods delivery
' that the payment was performed and providing him with
' all needed information about the payment. Some flags
' informing that user paid for a services can be also
' set here. For example, if user paid for registration
' on the site, then the flag should be set
' allowing the user who paid to access the site
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Catch ex As Exception
KBSoft.Carts.WriteFile(
"Error in IPNHandler: " + ex.Message)
End Try
Else
KBSoft.Carts.WriteFile(
"Error in IPNHandler. IPNResponse = 'INVALID'")
End If
End While
readStream.Close()
strResponse.Close()
End Sub
web.config 参数调整
要使用本文随附的源代码,您需要正确指定 web.config 参数。在 web.config 文件中调整参数时,您应该特别注意 appSettings
设置
<appSettings>
<!-- PayPay parameters-->
<add key="BusinessEmail" value="mymail@mail.com"/>
<add key="CancelPurchaseUrl" value="http://YOUR_IP/paypal/default.aspx"/>
<add key="ReturnUrl" value="http://YOUR_IP/paypal/payment_success.aspx"/>
<add key="NotifyUrl" value="http://YOUR_IP/paypal/IPNHandler.aspx"/>
<add key="CurrencyCode" value="USD"/>
<add key="UseSandbox" value="true"/>
<add key="SendToReturnURL" value="true"/>
</appSettings>
在 BusinessEmail
参数中指定收款人的电子邮件。这可以是创建 PayPal 账户时使用的电子邮件,也可以是账户参数中指定的备用电子邮件。通常在此参数中指定用作 PayPal 账户登录名的电子邮件。对于 CancelPurchaseUrl
、ReturnUrl
和 NotifyUrl
参数:在 YOUR_IP
中,您需要指定您的全球 IP 地址(而不是您在本地网络中的 IP 地址)或托管卖家网站的注册站点的域。在测试时,您可以简单地将 YOUR_IP
替换为 localhost
。需要注意的是,IPN 仅在您正确指定全球 IP 地址或注册域的名称时才能正常工作。如果您指定 localhost
,则只有 ReturnUrl
参数中指定的脚本才能支持与 PayPal 的交互。NotifyUrl
参数中指定的脚本将无法支持。您应该记住,IPN 可能会根据防火墙设置而被阻止。
以上链接是针对“paypal”虚拟目录给出的。如果您使用其他虚拟目录或站点,您需要用其名称替换“paypal”。要检查您的 NotifyUrl
是否有效,请在您的 PayPal Business 或 Premier 账户中,进入“个人资料”->“即时支付通知首选项”->“编辑”,并在“通知 URL:”字段中输入其地址。选中此页面上的复选框并点击“保存”按钮。如果 URL 有效,将显示一条关于成功验证的消息。
CurrencyCode
参数用于指定用于支付的货币代码。UseSandbox
参数用于在 PayPal 沙盒和真实 PayPal 账户之间切换。SendToReturnURL
参数用于开启/关闭向 return_url
发送通知。建议仅出于测试目的将 SendToReturnURL
设置为 true
。
源代码的使用
本文附带了简化在线商店的源代码档案。您可以在您的软件产品中使用此代码,以在需要执行支付时支持与 PayPal 的交互。
要使用本文提供的代码,您的系统上需要安装 .NET Framework 2.0 或更高版本。您还需要拥有真实或沙盒 PayPal 账户。开设沙盒账户无需额外要求。但是,要创建真实的 PayPal 账户,您需要一张信用卡和/或一个美国银行账户。请确保您的系统上正在运行标准 WebClient 服务。如果您有 Visual Studio 2005,可以通过打开 paypal.sln 解决方案并执行代码来运行测试。在这种情况下,IPN 将不可用。另一种选择是在 IIS 服务器上创建虚拟目录。
结论
最后,我想给您一些建议
- 在收到 PayPal 的
VERIFIED
响应之前,切勿信任 IPN 脚本获得的数据。已处理交易的信息应予以保留。因此,当收到VERIFIED
响应时,您可以确保该交易之前未被处理过。 - 不要使用
payer_email
来识别买家,因为电子邮件可能会更改。请使用payer_id
。 txn_type=web_accept
并不意味着卖家将收到付款。您应始终检查payment_status=completed
。唯一的例外是卖家账户非美国且pending_reason=intl
的情况。- 任何系统都可能发生故障,PayPal 也不例外。如果 IPN 脚本收到可疑数据,应将其写入日志并通知管理员。为用户提供一种报告错误的表单也很有用。
参考文献
- PayPal 帮助中心
- 这是一个独立的 PayPal 开发者论坛
- 此页面包含“立即购买”按钮的指南
- 此页面包含有关加密网站支付的指南
- 此页面包含使用 PayPal 沙盒进行测试的建议
- 此页面包含可用于创建 HTML 代码的变量,以便与 标准网站支付 一起使用
- 此页面包含本文的最新版本和文章中描述的示例的 C# 实现
历史
- 2007 年 6 月 14 日 -- 原始版本发布
- 2007 年 6 月 27 日 -- 添加了文章中描述的示例的 C# 实现(请参阅 此处)
- 2007 年 8 月 24 日 -- 源代码下载更新
- 2008 年 6 月 3 日 -- 文章更新