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:
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user