feat(order): 下单支持显式 client_type 端型(契约 v1.1.0)

resolveIsMobile:mobile/pc 显式优先,空值回退 isMobileUA(请求 UA),非法 400;
修复业务方服务端下单被 Go client UA 误判为 PC、手机用户拿不到 wap 拉起页

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
This commit is contained in:
wangjia
2026-07-04 10:28:17 +08:00
parent e0c7baa7cb
commit dbdadd1b10
2 changed files with 61 additions and 5 deletions
+23 -1
View File
@@ -32,6 +32,22 @@ func isMobileUA(ua string) bool {
return false
}
// resolveIsMobile 决定收银台端型:业务方显式 client_type 优先(服务端间调用时
// 下单请求 UA 是业务后端的,不代表终端用户,如 jiu 后端 Go client 恒为 PC UA);
// 缺省回退按下单请求 UA 判断(兼容旧接入方 / 浏览器直连 /paytest)。契约 v1.1.0。
func resolveIsMobile(clientType, ua string) (bool, error) {
switch clientType {
case "mobile":
return true, nil
case "pc":
return false, nil
case "":
return isMobileUA(ua), nil
default:
return false, errors.New("client_type 仅支持 pc / mobile")
}
}
type OrderHandler struct {
svc *service.OrderService
}
@@ -45,6 +61,7 @@ type createOrderRequest struct {
BizSystem string `json:"biz_system,omitempty"` // 业务对接来源(如 jiu);带则需签名鉴权
BizRef string `json:"biz_ref,omitempty"` // 业务引用(jiu purchase_id
ReturnURL string `json:"return_url,omitempty"` // 自定义付款后跳回地址
ClientType string `json:"client_type,omitempty"` // 显式端类型 pc/mobilev1.1.0);缺省按下单请求 UA 兜底
}
// verifyBizSign 校验业务方下单请求的 HMAC 签名(防外部乱下单/伪造业务单)。
@@ -99,8 +116,13 @@ func (h *OrderHandler) Create(c *gin.Context) {
return
}
}
isMobile, err := resolveIsMobile(req.ClientType, c.Request.UserAgent())
if err != nil {
util.RespondError(c, http.StatusBadRequest, "bad_request", err.Error())
return
}
payURL, order, err := h.svc.Create(c.Request.Context(), req.ProductID, c.ClientIP(),
isMobileUA(c.Request.UserAgent()),
isMobile,
service.BizParams{System: req.BizSystem, Ref: req.BizRef, ReturnURL: req.ReturnURL})
if err != nil {
if errors.Is(err, service.ErrProductNotFound) {
@@ -0,0 +1,34 @@
package handler
import "testing"
// 契约 v1.1.0client_type 显式端型优先,缺省回退 UA,非法值报错。
func TestResolveIsMobile(t *testing.T) {
const goUA = "Go-http-client/1.1"
const iphoneUA = "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) Mobile/15E148"
cases := []struct {
name string
clientType string
ua string
want bool
wantErr bool
}{
{"显式 mobile 覆盖服务端 UA", "mobile", goUA, true, false},
{"显式 pc 覆盖手机 UA", "pc", iphoneUA, false, false},
{"缺省回退:手机浏览器 UA", "", iphoneUA, true, false},
{"缺省回退:服务端 Go client UA 判 PC", "", goUA, false, false},
{"非法值报错", "tablet", iphoneUA, false, true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got, err := resolveIsMobile(tc.clientType, tc.ua)
if tc.wantErr != (err != nil) {
t.Fatalf("err = %v, wantErr = %v", err, tc.wantErr)
}
if !tc.wantErr && got != tc.want {
t.Fatalf("isMobile = %v, want %v", got, tc.want)
}
})
}
}