C# 如何接收 Incoming POST Data?
结论
application/x-www-form-urlencoded 数据无需手动读取请求体。如果回调方法是 ASP.NET Controller 的 Action,可以直接使用表单模型绑定,把 POST 字段映射到 CgCallback 对象。
Request 报错通常是因为该方法不在具有 HTTP 请求上下文的 Controller 中,或者当前类没有继承相应的 ASP.NET 基类。相比逐个读取 Request.Form,更适合使用 [FromForm] 接收数据。
ASP.NET Core 的写法
先定义回调模型。表单字段名是 order_id,而 C# 属性名是 OrderId,因此需要明确指定两者的映射关系:
using Microsoft.AspNetCore.Mvc;
public class CgCallback
{
[FromForm(Name = "id")]
public string? Id { get; set; }
[FromForm(Name = "order_id")]
public string? OrderId { get; set; }
}
然后在 Controller 中接收 POST 回调:
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("[controller]")]
public class PaymentController : ControllerBase
{
[HttpPost("/CgCallback")]
[Consumes("application/x-www-form-urlencoded")]
public IActionResult ReceiveCallback([FromForm] CgCallback callback)
{
if (string.IsNullOrWhiteSpace(callback.Id) ||
string.IsNullOrWhiteSpace(callback.OrderId))
{
return BadRequest("Missing required callback fields.");
}
// 在这里保存或处理 callback
// 例如:
// await _paymentService.ProcessCallbackAsync(callback);
return Ok(callback);
}
}
回调地址是:
POST https://example.com/CgCallback
假设请求体为:
id=123&order_id=ORDER-456
框架会映射为:
callback.Id // "123"
callback.OrderId // "ORDER-456"
如果属性上的 [FromForm] 不生效,也可以使用通用的模型绑定名称:
using Microsoft.AspNetCore.Mvc.ModelBinding;
public class CgCallback
{
[BindNever]
public string? InternalValue { get; set; }
[BindProperty(Name = "id")]
public string? Id { get; set; }
[BindProperty(Name = "order_id")]
public string? OrderId { get; set; }
}
不过在 ASP.NET Core Web API 中,通常直接在 Action 参数上使用 [FromForm],再为名称不一致的字段指定映射名称。这样写更直观。
手动读取表单字段
如果回调字段不固定,或者需要访问原始表单集合,可以在 Controller Action 中读取 Request.Form:
[HttpPost("/CgCallback")]
public async Task<IActionResult> ReceiveCallback()
{
if (!Request.HasFormContentType)
{
return BadRequest("Expected form-urlencoded data.");
}
IFormCollection form = await Request.ReadFormAsync();
var callback = new CgCallback
{
Id = form["id"].ToString(),
OrderId = form["order_id"].ToString()
};
return Ok(callback);
}
这里可以使用 Request,因为该方法位于继承自 ControllerBase 或 Controller 的 Controller 中。
不要把接收回调的方法写成普通类方法:
public CgCallback IncomingData()
{
// 普通类中没有 Controller 提供的 Request 属性
}
如果业务处理逻辑位于普通服务类中,应先由 Controller 接收请求,再把模型传给服务:
[HttpPost("/CgCallback")]
public IActionResult ReceiveCallback([FromForm] CgCallback callback)
{
_paymentService.Process(callback);
return Ok();
}
ASP.NET Framework MVC 的写法
如果项目使用旧版 ASP.NET MVC,而不是 ASP.NET Core,可以直接通过 Action 参数进行模型绑定:
public class CgCallback
{
public string Id { get; set; }
public string OrderId { get; set; }
}
using System.Web.Mvc;
public class PaymentController : Controller
{
[HttpPost]
public ActionResult CgCallback()
{
var callback = new CgCallback
{
Id = Request.Form["id"],
OrderId = Request.Form["order_id"]
};
return Json(callback);
}
}
此时,Request 来自 System.Web.Mvc.Controller。如果当前类没有继承 Controller,同样会出现以下错误:
The name 'Request' does not exist in the current context
路由需要与回调地址一致
只把方法命名为 CgCallback,不一定会自动生成 /CgCallback 路由。在 ASP.NET Core 中,可以明确声明绝对路由:
[HttpPost("/CgCallback")]
public IActionResult ReceiveCallback([FromForm] CgCallback callback)
{
return Ok();
}
也可以使用 Controller 路由:
[Route("payment")]
public class PaymentController : ControllerBase
{
[HttpPost("callback")]
public IActionResult ReceiveCallback([FromForm] CgCallback callback)
{
return Ok();
}
}
对应地址是:
https://example.com/payment/callback
注意事项
支付回调不能只根据 id 和 order_id 更新订单状态,还需要注意以下事项:
- 按照 API 文档验证签名、时间戳或回调令牌。
- 核对订单号、金额、币种和支付状态是否与本地订单一致。
- 做好幂等处理,因为支付平台可能重复发送同一回调。
- 不要只根据客户端跳转页面判断支付是否成功。
- 按照 API 要求返回指定的状态码或确认文本。某些平台要求
200 OK,另一些平台要求返回固定字符串,具体以对方的接口文档为准。 - 不要在日志中记录银行卡信息、密钥、完整签名或其他敏感数据。
对于当前场景,ASP.NET Core 中可以采用下面的简洁写法:
[HttpPost("/CgCallback")]
public IActionResult ReceiveCallback([FromForm] CgCallback callback)
{
// 验证并处理 callback
return Ok();
}
该方法必须是 Controller Action,并通过 [FromForm] 告诉 ASP.NET Core 从 application/x-www-form-urlencoded 表单中绑定模型。
备注:内容仅供参考。
