feat: 购买下单透传终端类型,手机端收银台拉起支付宝 App(契约 v1.1.0)
根因:pay 判手机/PC 用下单请求 UA,而下单由 jiu 后端 Go client 发出, 恒被判 PC 扫码页——手机用户拿不到 wap 拉起页。 - backend:/license/purchase 请求体加可选 client_type(oneof=pc mobile), 缺省按本请求 UA 兜底(官网浏览器购买自动受益);CreatePurchase 透传给 pay; 单测断言透传 + 既有用例补参 - client:createPurchase 支持 clientType;PurchaseCard 按平台声明 (iOS/Android=mobile,桌面=pc,Web 不传走 UA),kIsWeb 先于 dart:io; launchUrl 外部浏览器不变,wap 收银台自动拉起支付宝 - 配套:pay-contract v1.1.0(744725b)+ pay 侧 resolveIsMobile(dbdadd1)已各自提交 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
This commit is contained in:
@@ -90,7 +90,10 @@ type PurchaseResult struct {
|
||||
}
|
||||
|
||||
// CreatePurchase 建购买记录并调 pay 下单,返回收银台跳转 URL。
|
||||
func (s *PayService) CreatePurchase(shopID, userID uint64, bizCode string) (*PurchaseResult, error) {
|
||||
// clientType(契约 v1.1.0,"pc"/"mobile"/""):显式端型透传给 pay 决定收银台形态
|
||||
// (mobile=手机网站支付拉起支付宝 App / pc=电脑扫码页);服务端间调用下单请求 UA
|
||||
// 是本后端的,不传则 pay 会按 Go client UA 误判为 PC。
|
||||
func (s *PayService) CreatePurchase(shopID, userID uint64, bizCode, clientType string) (*PurchaseResult, error) {
|
||||
if !s.Configured() {
|
||||
return nil, ErrPayNotConfigured
|
||||
}
|
||||
@@ -117,12 +120,16 @@ func (s *PayService) CreatePurchase(shopID, userID uint64, bizCode string) (*Pur
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reqBody, _ := json.Marshal(map[string]any{
|
||||
payload := map[string]any{
|
||||
"product_id": productID,
|
||||
"biz_system": "jiu",
|
||||
"biz_ref": strconv.FormatUint(p.ID, 10),
|
||||
"return_url": s.retURL,
|
||||
})
|
||||
}
|
||||
if clientType != "" {
|
||||
payload["client_type"] = clientType
|
||||
}
|
||||
reqBody, _ := json.Marshal(payload)
|
||||
respBody, err := s.signedPost("/api/v1/orders", reqBody)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("pay 下单失败: %w", err)
|
||||
|
||||
@@ -234,7 +234,7 @@ func TestCreatePurchase_HappyPath(t *testing.T) {
|
||||
mux.HandleFunc("GET /api/v1/products", func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, `{"data":[{"id":3,"biz_code":"annual_standard"},{"id":4,"biz_code":"monthly_pro"}]}`)
|
||||
})
|
||||
var gotBizRef string
|
||||
var gotBizRef, gotClientType string
|
||||
mux.HandleFunc("POST /api/v1/orders", func(w http.ResponseWriter, r *http.Request) {
|
||||
body := make([]byte, r.ContentLength)
|
||||
_, _ = r.Body.Read(body)
|
||||
@@ -246,13 +246,14 @@ func TestCreatePurchase_HappyPath(t *testing.T) {
|
||||
var req map[string]any
|
||||
_ = json.Unmarshal(body, &req)
|
||||
gotBizRef, _ = req["biz_ref"].(string)
|
||||
gotClientType, _ = req["client_type"].(string)
|
||||
fmt.Fprint(w, `{"data":{"pay_url":"https://openapi.alipay.com/gateway","out_trade_no":"yanmei-new-1","amount":"2999.00","subject":"年付标准"}}`)
|
||||
})
|
||||
payServer := httptest.NewServer(mux)
|
||||
defer payServer.Close()
|
||||
|
||||
svc := newTestPaySvc(db, payServer.URL)
|
||||
res, err := svc.CreatePurchase(shop.ID, 1, "annual_standard")
|
||||
res, err := svc.CreatePurchase(shop.ID, 1, "annual_standard", "mobile")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "https://openapi.alipay.com/gateway", res.PayURL)
|
||||
assert.Equal(t, "yanmei-new-1", res.OutTradeNo)
|
||||
@@ -262,16 +263,17 @@ func TestCreatePurchase_HappyPath(t *testing.T) {
|
||||
assert.Equal(t, "pending", p.Status)
|
||||
assert.Equal(t, "2999.00", p.Amount)
|
||||
assert.Equal(t, strconv.FormatUint(p.ID, 10), gotBizRef, "biz_ref 应为购买记录 id")
|
||||
assert.Equal(t, "mobile", gotClientType, "client_type 应透传给 pay(契约 v1.1.0)")
|
||||
}
|
||||
|
||||
func TestCreatePurchase_UnknownPlanAndUnconfigured(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
svc := newTestPaySvc(db, "http://pay.invalid")
|
||||
_, err := svc.CreatePurchase(1, 1, "no_such_plan")
|
||||
_, err := svc.CreatePurchase(1, 1, "no_such_plan", "pc")
|
||||
assert.ErrorIs(t, err, ErrUnknownPlan)
|
||||
|
||||
unconfigured := NewPayService(db, "http://pay.invalid", "", "")
|
||||
_, err = unconfigured.CreatePurchase(1, 1, "annual_standard")
|
||||
_, err = unconfigured.CreatePurchase(1, 1, "annual_standard", "pc")
|
||||
assert.ErrorIs(t, err, ErrPayNotConfigured)
|
||||
}
|
||||
|
||||
@@ -287,7 +289,7 @@ func TestCreatePurchase_PromoOncePerShop(t *testing.T) {
|
||||
used, err := svc.PromoUsed(shop.ID)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, used)
|
||||
_, err = svc.CreatePurchase(shop.ID, 1, PromoBizCode)
|
||||
_, err = svc.CreatePurchase(shop.ID, 1, PromoBizCode, "pc")
|
||||
assert.NotErrorIs(t, err, ErrPromoUsed)
|
||||
|
||||
// pending 单不算已享用(可能弃单),仍可重新下单
|
||||
@@ -302,7 +304,7 @@ func TestCreatePurchase_PromoOncePerShop(t *testing.T) {
|
||||
used, err = svc.PromoUsed(shop.ID)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, used)
|
||||
_, err = svc.CreatePurchase(shop.ID, 1, PromoBizCode)
|
||||
_, err = svc.CreatePurchase(shop.ID, 1, PromoBizCode, "pc")
|
||||
assert.ErrorIs(t, err, ErrPromoUsed)
|
||||
|
||||
// 多租户隔离:别家买过不影响本店
|
||||
|
||||
Reference in New Issue
Block a user