PAYATHON 2026

Golang 请求手机网站支付接口2.0为何返回完整 HTML 页面

支付小周

结论

手机网站支付接口 2.0 是“页面跳转类”接口,不是普通的 JSON 查询接口。它返回的通常是一段由浏览器渲染并自动提交的 HTML,其中可能包含 <form>、隐藏字段和 JavaScript。因此,收到完整的 HTML 文档并不一定代表接口异常。

如果程序收到的是表单提交后的收银台页面,常见原因是 Go 的 http.Client 默认跟随了 302、303 等重定向。此时程序读取的是跳转后的响应,而非第一次响应。建议按以下方式处理:

  1. 不要让服务端 HTTP 客户端代替用户浏览器完成支付跳转。
  2. 检查是否发生了自动重定向,并根据需要将其关闭。
  3. 将接口生成的 HTML 表单原样返回给前端浏览器,由浏览器提交并进入支付页面。
  4. 不要用正则表达式从完整 HTML 中截取 <form>。

为什么会返回完整 HTML

手机网站支付需要把用户带到支付收银台,调用流程与普通 API 不同:

业务服务端生成并签名支付参数
        ↓
生成 HTML form 或支付跳转页面
        ↓
将 HTML 返回给用户浏览器
        ↓
浏览器提交 form
        ↓
进入支付收银台

在线调试工具展示“接口返回代码”时,可能只突出表单部分,但这不代表正式请求只会返回一个孤立的 <form> 标签。实际响应可能是一份完整的 HTML 文档,例如:

<!doctype html>
<html>
<head>
    <meta charset="utf-8">
</head>
<body>
<form name="punchout_form" method="post" action="...">
    <input type="hidden" name="..." value="...">
</form>
<script>
    document.forms[0].submit();
</script>
</body>
</html>

这类响应需要由浏览器解析和执行。Go 的普通 HTTP 客户端不会执行其中的 JavaScript,也不会因为 HTML 中有 <form> 就自动提交表单。

但 Go 会默认跟随 HTTP 重定向。如果支付网关先返回 302 或 303,http.Client 可能继续请求 Location 指向的地址,程序最终拿到的就是收银台完整页面的 HTML。

先确认是否发生了自动重定向

可以暂时关闭重定向,然后检查首次响应的状态码、Location 和 Content-Type:

package main

import (
	"fmt"
	"io"
	"net/http"
	"net/url"
)

func requestPayment(gatewayURL string, params url.Values) ([]byte, error) {
	client := &http.Client{
		CheckRedirect: func(req *http.Request, via []*http.Request) error {
			// 保留第一次重定向响应,不继续访问 Location。
			return http.ErrUseLastResponse
		},
	}

	resp, err := client.PostForm(gatewayURL, params)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	fmt.Println("status:", resp.Status)
	fmt.Println("content-type:", resp.Header.Get("Content-Type"))
	fmt.Println("location:", resp.Header.Get("Location"))

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}

	return body, nil
}

可以根据响应判断具体情况:

  • 如果状态码是 302 或 303,并且响应中有 Location,说明此前很可能发生了自动跳转。
  • 如果首次响应就是 200 OK,Content-Type 为 text/html,正文中还包含表单和自动提交脚本,那么完整 HTML 通常就是页面支付接口的正常返回结果。
  • 如果收到的是错误页面、登录页面或网关提示页面,需要继续检查请求地址、签名、字符集、沙箱配置和必填参数。

调试时也可以输出实际响应地址:

fmt.Println("final URL:", resp.Request.URL.String())

如果这个地址不是最初请求的网关地址,说明客户端已经跟随过重定向。

正确处理返回的表单

方式一:使用 SDK 生成页面表单

如果使用的 Go SDK 提供 pageExecute、PageExecute 或“生成 WAP 支付表单”之类的方法,应优先调用该方法。不同 SDK 的具体方法名可能不同,不能直接照搬。

通常的做法是取得 SDK 生成的 HTML,再将其原样返回给浏览器:

func payHandler(w http.ResponseWriter, r *http.Request) {
	// formHTML 应由所使用的支付 SDK 根据已签名参数生成。
	formHTML, err := buildPaymentForm()
	if err != nil {
		http.Error(w, "生成支付请求失败", http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	w.WriteHeader(http.StatusOK)

	if _, err := w.Write([]byte(formHTML)); err != nil {
		// 此处按项目的日志规范记录错误。
		return
	}
}

浏览器收到响应后,会渲染表单,并按照页面中的脚本自动提交。

方式二:自行生成自动提交表单

如果 SDK 只负责生成参数和签名,也可以按照接口规范自行生成表单。参数值必须经过 HTML 转义,不能直接拼接未经处理的业务数据:

package main

import (
	"bytes"
	"html/template"
)

var paymentFormTemplate = template.Must(template.New("payment").Parse(`
<!doctype html>
<html>
<head>
    <meta charset="utf-8">
    <title>正在跳转到支付页面</title>
</head>
<body>
<form id="paymentForm" method="post" action="{{.Action}}">
    {{range .Fields}}
    <input type="hidden" name="{{.Name}}" value="{{.Value}}">
    {{end}}
</form>
<script>
    document.getElementById("paymentForm").submit();
</script>
</body>
</html>
`))

type FormField struct {
	Name  string
	Value string
}

type PaymentForm struct {
	Action string
	Fields []FormField
}

func renderPaymentForm(action string, fields []FormField) (string, error) {
	var output bytes.Buffer

	err := paymentFormTemplate.Execute(&output, PaymentForm{
		Action: action,
		Fields: fields,
	})
	if err != nil {
		return "", err
	}

	return output.String(), nil
}

签名参数的生成方式、参数排序、编码规则和签名算法,必须以当前接口规范及所用 SDK 的要求为准,不能只参照上面的 HTML 示例实现。

不建议截取 <form> 字符串

不要用正则表达式从完整页面中截取 <form>...</form>。支付页面还可能依赖以下内容:

  • 自动提交脚本;
  • 指定的字符编码;
  • 页面中的其他隐藏字段;
  • 特定的表单提交方式;
  • 后续版本新增的页面结构。

仅保留 <form> 可能导致页面无法自动提交,也可能遗漏必要信息。只要响应来源可信,并且确认页面由支付接口生成,通常应该原样返回整份 HTML。

注意事项

  1. 沙箱网关和正式网关不能混用,对应的应用配置、密钥和账号也要保持一致。
  2. return_url 只用于用户完成支付后的页面跳转,不能作为判断支付结果的唯一依据。
  3. 最终支付状态应通过异步通知或主动查询接口确认,同时验证通知签名。
  4. 不要在日志中完整记录签名、用户隐私、订单敏感字段或支付页面的全部隐藏参数。
  5. 如果通过 AJAX 发起支付请求,浏览器通常不会自动用返回的 HTML 替换当前页面。更稳妥的做法是直接导航到后端支付入口,或者由前端将返回的 HTML 写入新页面,再提交表单。
  6. 如果关闭重定向后,首次响应仍然是完整 HTML,通常说明网关返回的本来就是可执行的支付页面,并不是 Go 客户端误提交了表单。

备注:内容仅供参考。