diff --git a/internal/handler/order.go b/internal/handler/order.go index 55074ed..c65be88 100644 --- a/internal/handler/order.go +++ b/internal/handler/order.go @@ -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 } @@ -41,10 +57,11 @@ func NewOrderHandler(svc *service.OrderService) *OrderHandler { } type createOrderRequest struct { - ProductID uint64 `json:"product_id"` - BizSystem string `json:"biz_system,omitempty"` // 业务对接来源(如 jiu);带则需签名鉴权 - BizRef string `json:"biz_ref,omitempty"` // 业务引用(jiu purchase_id) - ReturnURL string `json:"return_url,omitempty"` // 自定义付款后跳回地址 + ProductID uint64 `json:"product_id"` + 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/mobile(v1.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) { diff --git a/internal/handler/order_client_type_test.go b/internal/handler/order_client_type_test.go new file mode 100644 index 0000000..a7b307a --- /dev/null +++ b/internal/handler/order_client_type_test.go @@ -0,0 +1,34 @@ +package handler + +import "testing" + +// 契约 v1.1.0:client_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) + } + }) + } +}