fix(upgrade): close payment and oidc compatibility gaps
This commit is contained in:
@@ -200,14 +200,7 @@ func (w *Wxpay) CreatePayment(ctx context.Context, req payment.CreatePaymentRequ
|
|||||||
case wxpayModeJSAPI:
|
case wxpayModeJSAPI:
|
||||||
return w.prepayJSAPI(ctx, client, req, notifyURL, totalFen)
|
return w.prepayJSAPI(ctx, client, req, notifyURL, totalFen)
|
||||||
case wxpayModeH5:
|
case wxpayModeH5:
|
||||||
resp, err := w.prepayH5(ctx, client, req, notifyURL, totalFen)
|
return w.prepayH5(ctx, client, req, notifyURL, totalFen)
|
||||||
if err == nil {
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
if wxpayShouldFallbackToNative(err) {
|
|
||||||
return w.prepayNativeFallback(ctx, client, req, notifyURL, totalFen)
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
case wxpayModeNative:
|
case wxpayModeNative:
|
||||||
return w.prepayNative(ctx, client, req, notifyURL, totalFen)
|
return w.prepayNative(ctx, client, req, notifyURL, totalFen)
|
||||||
default:
|
default:
|
||||||
@@ -292,23 +285,6 @@ func (w *Wxpay) prepayH5(ctx context.Context, c *core.Client, req payment.Create
|
|||||||
return &payment.CreatePaymentResponse{TradeNo: req.OrderID, PayURL: h5URL}, nil
|
return &payment.CreatePaymentResponse{TradeNo: req.OrderID, PayURL: h5URL}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *Wxpay) prepayNativeFallback(ctx context.Context, c *core.Client, req payment.CreatePaymentRequest, notifyURL string, totalFen int64) (*payment.CreatePaymentResponse, error) {
|
|
||||||
resp, err := w.prepayNative(ctx, c, req, notifyURL, totalFen)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("wxpay native fallback after NO_AUTH: %w", err)
|
|
||||||
}
|
|
||||||
nativeURL := strings.TrimSpace(resp.PayURL)
|
|
||||||
if nativeURL == "" {
|
|
||||||
nativeURL = strings.TrimSpace(resp.QRCode)
|
|
||||||
}
|
|
||||||
if nativeURL == "" {
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
resp.PayURL = nativeURL
|
|
||||||
resp.QRCode = nativeURL
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildWxpayH5Info(config map[string]string) *h5.H5Info {
|
func buildWxpayH5Info(config map[string]string) *h5.H5Info {
|
||||||
tp := wxpayH5Type
|
tp := wxpayH5Type
|
||||||
info := &h5.H5Info{Type: &tp}
|
info := &h5.H5Info{Type: &tp}
|
||||||
@@ -321,10 +297,6 @@ func buildWxpayH5Info(config map[string]string) *h5.H5Info {
|
|||||||
return info
|
return info
|
||||||
}
|
}
|
||||||
|
|
||||||
func wxpayShouldFallbackToNative(err error) bool {
|
|
||||||
return err != nil && strings.Contains(err.Error(), wxpayErrNoAuth)
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolveWxpayCreateMode(req payment.CreatePaymentRequest) (string, error) {
|
func resolveWxpayCreateMode(req payment.CreatePaymentRequest) (string, error) {
|
||||||
if strings.TrimSpace(req.OpenID) != "" {
|
if strings.TrimSpace(req.OpenID) != "" {
|
||||||
return wxpayModeJSAPI, nil
|
return wxpayModeJSAPI, nil
|
||||||
|
|||||||
@@ -643,7 +643,7 @@ func TestCreatePaymentMobileH5IncludesConfiguredSceneInfo(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCreatePaymentMobileH5FallsBackToNativeOnNoAuth(t *testing.T) {
|
func TestCreatePaymentMobileH5ReturnsNoAuthErrorWithoutNativeFallback(t *testing.T) {
|
||||||
origJSAPIPrepay := wxpayJSAPIPrepayWithRequestPayment
|
origJSAPIPrepay := wxpayJSAPIPrepayWithRequestPayment
|
||||||
origNativePrepay := wxpayNativePrepay
|
origNativePrepay := wxpayNativePrepay
|
||||||
origH5Prepay := wxpayH5Prepay
|
origH5Prepay := wxpayH5Prepay
|
||||||
@@ -688,8 +688,8 @@ func TestCreatePaymentMobileH5FallsBackToNativeOnNoAuth(t *testing.T) {
|
|||||||
ClientIP: "203.0.113.10",
|
ClientIP: "203.0.113.10",
|
||||||
IsMobile: true,
|
IsMobile: true,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err == nil {
|
||||||
t.Fatalf("unexpected error: %v", err)
|
t.Fatal("expected no-auth error, got nil")
|
||||||
}
|
}
|
||||||
if jsapiCalls != 0 {
|
if jsapiCalls != 0 {
|
||||||
t.Fatalf("jsapi prepay calls = %d, want 0", jsapiCalls)
|
t.Fatalf("jsapi prepay calls = %d, want 0", jsapiCalls)
|
||||||
@@ -697,13 +697,13 @@ func TestCreatePaymentMobileH5FallsBackToNativeOnNoAuth(t *testing.T) {
|
|||||||
if h5Calls != 1 {
|
if h5Calls != 1 {
|
||||||
t.Fatalf("h5 prepay calls = %d, want 1", h5Calls)
|
t.Fatalf("h5 prepay calls = %d, want 1", h5Calls)
|
||||||
}
|
}
|
||||||
if nativeCalls != 1 {
|
if nativeCalls != 0 {
|
||||||
t.Fatalf("native prepay calls = %d, want 1", nativeCalls)
|
t.Fatalf("native prepay calls = %d, want 0", nativeCalls)
|
||||||
}
|
}
|
||||||
if resp.PayURL != "weixin://wxpay/bizpayurl?pr=fallback-native" {
|
if resp != nil {
|
||||||
t.Fatalf("pay_url = %q, want native fallback url", resp.PayURL)
|
t.Fatalf("expected nil response, got %+v", resp)
|
||||||
}
|
}
|
||||||
if resp.QRCode != "weixin://wxpay/bizpayurl?pr=fallback-native" {
|
if !strings.Contains(err.Error(), "NO_AUTH") {
|
||||||
t.Fatalf("qr_code = %q, want native fallback url", resp.QRCode)
|
t.Fatalf("error = %v, want NO_AUTH", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,10 +66,12 @@ type migrationChecksumCompatibilityRule struct {
|
|||||||
var migrationChecksumCompatibilityRules = map[string]migrationChecksumCompatibilityRule{
|
var migrationChecksumCompatibilityRules = map[string]migrationChecksumCompatibilityRule{
|
||||||
"054_drop_legacy_cache_columns.sql": newMigrationChecksumCompatibilityRule("82de761156e03876653e7a6a4eee883cd927847036f779b0b9f34c42a8af7a7d", "182c193f3359946cf094090cd9e57d5c3fd9abaffbc1e8fc378646b8a6fa12b4"),
|
"054_drop_legacy_cache_columns.sql": newMigrationChecksumCompatibilityRule("82de761156e03876653e7a6a4eee883cd927847036f779b0b9f34c42a8af7a7d", "182c193f3359946cf094090cd9e57d5c3fd9abaffbc1e8fc378646b8a6fa12b4"),
|
||||||
"061_add_usage_log_request_type.sql": newMigrationChecksumCompatibilityRule("66207e7aa5dd0429c2e2c0fabdaf79783ff157fa0af2e81adff2ee03790ec65c", "08a248652cbab7cfde147fc6ef8cda464f2477674e20b718312faa252e0481c0", "222b4a09c797c22e5922b6b172327c824f5463aaa8760e4f621bc5c22e2be0f3"),
|
"061_add_usage_log_request_type.sql": newMigrationChecksumCompatibilityRule("66207e7aa5dd0429c2e2c0fabdaf79783ff157fa0af2e81adff2ee03790ec65c", "08a248652cbab7cfde147fc6ef8cda464f2477674e20b718312faa252e0481c0", "222b4a09c797c22e5922b6b172327c824f5463aaa8760e4f621bc5c22e2be0f3"),
|
||||||
"109_auth_identity_compat_backfill.sql": newMigrationChecksumCompatibilityRule("2b380305e73ff0c13aa8c811e45897f2b36ca4a438f7b3e8f98e19ecb6bae0b3", "551e498aa5616d2d91096e9d72cf9fb36e418ee22eacc557f8811cadbc9e20ee"),
|
"109_auth_identity_compat_backfill.sql": newMigrationChecksumCompatibilityRule("0580b4602d85435edf9aca1633db580bb3932f26517f75134106f80275ec2ace", "551e498aa5616d2d91096e9d72cf9fb36e418ee22eacc557f8811cadbc9e20ee"),
|
||||||
|
"110_pending_auth_and_provider_default_grants.sql": newMigrationChecksumCompatibilityRule("32cf87ee787b1bb36b5c691367c96eee37518fa3eed6f3322cf68795e3745279", "e3d1f433be2b564cfbdc549adf98fce13c5c7b363ebc20fd05b765d0563b0925"),
|
||||||
|
"112_add_payment_order_provider_key_snapshot.sql": newMigrationChecksumCompatibilityRule("b75f8f56d39455682787696a3d92ad25b055444ca328fb7fca9a460a15d68d99", "ffd3e8a2c9295fa9cbefefd629a78268877e5b51bc970a82d9b3f46ec4ebd15e"),
|
||||||
"115_auth_identity_legacy_external_backfill.sql": newMigrationChecksumCompatibilityRule("022aadd97bb53e755f0cf7a3a957e0cb1a1353b0c39ec4de3234acd2871fd04f", "4cf39e508be9fd1a5aa41610cbbebeb80385c9adda45bf78a706de9db4f1385f"),
|
"115_auth_identity_legacy_external_backfill.sql": newMigrationChecksumCompatibilityRule("022aadd97bb53e755f0cf7a3a957e0cb1a1353b0c39ec4de3234acd2871fd04f", "4cf39e508be9fd1a5aa41610cbbebeb80385c9adda45bf78a706de9db4f1385f"),
|
||||||
"116_auth_identity_legacy_external_safety_reports.sql": newMigrationChecksumCompatibilityRule("07edb09fa8d04ffb172b0621e3c22f4d1757d20a24ae267b3b36b087ab72d488", "f7757bd929ac67ffb08ce69fa4cf20fad39dbff9d5a5085fb2adabb7607e5877"),
|
"116_auth_identity_legacy_external_safety_reports.sql": newMigrationChecksumCompatibilityRule("07edb09fa8d04ffb172b0621e3c22f4d1757d20a24ae267b3b36b087ab72d488", "f7757bd929ac67ffb08ce69fa4cf20fad39dbff9d5a5085fb2adabb7607e5877"),
|
||||||
"118_wechat_dual_mode_and_auth_source_defaults.sql": newMigrationChecksumCompatibilityRule("b54194d7a3e4fbf710e0a3590d22a2fe7966804c487052a356e0b55f53ef96b0", "e0cdf835d6c688d64100f483d31bc02ac9ebad414bf1837af239a84bf75b8227"),
|
"118_wechat_dual_mode_and_auth_source_defaults.sql": newMigrationChecksumCompatibilityRule("b54194d7a3e4fbf710e0a3590d22a2fe7966804c487052a356e0b55f53ef96b0", "e0cdf835d6c688d64100f483d31bc02ac9ebad414bf1837af239a84bf75b8227", "a38243ca0a72c3a01c0a92b7986423054d6133c0399441f853b99802852720fb"),
|
||||||
"119_enforce_payment_orders_out_trade_no_unique.sql": newMigrationChecksumCompatibilityRule("0bbe809ae48a9d811dabda1ba1c74955bd71c4a9cc610f9128816818dfa6c11e", "ebd2c67cce0116393fb4f1b5d5116a67c6aceb73820dfb5133d1ff6f36d72d34"),
|
"119_enforce_payment_orders_out_trade_no_unique.sql": newMigrationChecksumCompatibilityRule("0bbe809ae48a9d811dabda1ba1c74955bd71c4a9cc610f9128816818dfa6c11e", "ebd2c67cce0116393fb4f1b5d5116a67c6aceb73820dfb5133d1ff6f36d72d34"),
|
||||||
"120_enforce_payment_orders_out_trade_no_unique_notx.sql": newMigrationChecksumCompatibilityRule("34aadc0db59a4e390f92a12b73bd74642d9724f33124f73638ae00089ea5e074", "e77921f79d539bc24575cb9c16cbe566d2b23ce816190343d0a7568f6a3fcf61", "707431450603e70a43ce9fbd61e0c12fa67da4875158ccefabacea069587ab22", "04b082b5a239c525154fe9185d324ee2b05ff90da9297e10dba19f9be79aa59a"),
|
"120_enforce_payment_orders_out_trade_no_unique_notx.sql": newMigrationChecksumCompatibilityRule("34aadc0db59a4e390f92a12b73bd74642d9724f33124f73638ae00089ea5e074", "e77921f79d539bc24575cb9c16cbe566d2b23ce816190343d0a7568f6a3fcf61", "707431450603e70a43ce9fbd61e0c12fa67da4875158ccefabacea069587ab22", "04b082b5a239c525154fe9185d324ee2b05ff90da9297e10dba19f9be79aa59a"),
|
||||||
"123_fix_legacy_auth_source_grant_on_signup_defaults.sql": newMigrationChecksumCompatibilityRule("2ce43c2cd89e9f9e1febd34a407ed9e84d177386c5544b6f02c1f58a21129f57", "6cd33422f215dcd1f486ab6f35c0ea5805d9ca69bb25906d94bc649156657145"),
|
"123_fix_legacy_auth_source_grant_on_signup_defaults.sql": newMigrationChecksumCompatibilityRule("2ce43c2cd89e9f9e1febd34a407ed9e84d177386c5544b6f02c1f58a21129f57", "6cd33422f215dcd1f486ab6f35c0ea5805d9ca69bb25906d94bc649156657145"),
|
||||||
|
|||||||
@@ -55,8 +55,17 @@ func TestIsMigrationChecksumCompatible(t *testing.T) {
|
|||||||
t.Run("109历史checksum可兼容", func(t *testing.T) {
|
t.Run("109历史checksum可兼容", func(t *testing.T) {
|
||||||
ok := isMigrationChecksumCompatible(
|
ok := isMigrationChecksumCompatible(
|
||||||
"109_auth_identity_compat_backfill.sql",
|
"109_auth_identity_compat_backfill.sql",
|
||||||
"2b380305e73ff0c13aa8c811e45897f2b36ca4a438f7b3e8f98e19ecb6bae0b3",
|
|
||||||
"551e498aa5616d2d91096e9d72cf9fb36e418ee22eacc557f8811cadbc9e20ee",
|
"551e498aa5616d2d91096e9d72cf9fb36e418ee22eacc557f8811cadbc9e20ee",
|
||||||
|
"0580b4602d85435edf9aca1633db580bb3932f26517f75134106f80275ec2ace",
|
||||||
|
)
|
||||||
|
require.True(t, ok)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("109当前checksum可兼容历史checksum", func(t *testing.T) {
|
||||||
|
ok := isMigrationChecksumCompatible(
|
||||||
|
"109_auth_identity_compat_backfill.sql",
|
||||||
|
"551e498aa5616d2d91096e9d72cf9fb36e418ee22eacc557f8811cadbc9e20ee",
|
||||||
|
"0580b4602d85435edf9aca1633db580bb3932f26517f75134106f80275ec2ace",
|
||||||
)
|
)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
})
|
})
|
||||||
@@ -64,8 +73,26 @@ func TestIsMigrationChecksumCompatible(t *testing.T) {
|
|||||||
t.Run("109回滚到历史文件后仍兼容已应用的新checksum", func(t *testing.T) {
|
t.Run("109回滚到历史文件后仍兼容已应用的新checksum", func(t *testing.T) {
|
||||||
ok := isMigrationChecksumCompatible(
|
ok := isMigrationChecksumCompatible(
|
||||||
"109_auth_identity_compat_backfill.sql",
|
"109_auth_identity_compat_backfill.sql",
|
||||||
|
"0580b4602d85435edf9aca1633db580bb3932f26517f75134106f80275ec2ace",
|
||||||
"551e498aa5616d2d91096e9d72cf9fb36e418ee22eacc557f8811cadbc9e20ee",
|
"551e498aa5616d2d91096e9d72cf9fb36e418ee22eacc557f8811cadbc9e20ee",
|
||||||
"2b380305e73ff0c13aa8c811e45897f2b36ca4a438f7b3e8f98e19ecb6bae0b3",
|
)
|
||||||
|
require.True(t, ok)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("110历史checksum可兼容", func(t *testing.T) {
|
||||||
|
ok := isMigrationChecksumCompatible(
|
||||||
|
"110_pending_auth_and_provider_default_grants.sql",
|
||||||
|
"e3d1f433be2b564cfbdc549adf98fce13c5c7b363ebc20fd05b765d0563b0925",
|
||||||
|
"32cf87ee787b1bb36b5c691367c96eee37518fa3eed6f3322cf68795e3745279",
|
||||||
|
)
|
||||||
|
require.True(t, ok)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("112历史checksum可兼容", func(t *testing.T) {
|
||||||
|
ok := isMigrationChecksumCompatible(
|
||||||
|
"112_add_payment_order_provider_key_snapshot.sql",
|
||||||
|
"ffd3e8a2c9295fa9cbefefd629a78268877e5b51bc970a82d9b3f46ec4ebd15e",
|
||||||
|
"b75f8f56d39455682787696a3d92ad25b055444ca328fb7fca9a460a15d68d99",
|
||||||
)
|
)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
})
|
})
|
||||||
@@ -97,6 +124,20 @@ func TestIsMigrationChecksumCompatible(t *testing.T) {
|
|||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("118多个历史checksum都可兼容当前版本", func(t *testing.T) {
|
||||||
|
for _, dbChecksum := range []string{
|
||||||
|
"a38243ca0a72c3a01c0a92b7986423054d6133c0399441f853b99802852720fb",
|
||||||
|
"e0cdf835d6c688d64100f483d31bc02ac9ebad414bf1837af239a84bf75b8227",
|
||||||
|
} {
|
||||||
|
ok := isMigrationChecksumCompatible(
|
||||||
|
"118_wechat_dual_mode_and_auth_source_defaults.sql",
|
||||||
|
dbChecksum,
|
||||||
|
"b54194d7a3e4fbf710e0a3590d22a2fe7966804c487052a356e0b55f53ef96b0",
|
||||||
|
)
|
||||||
|
require.True(t, ok)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("120多个历史checksum都可兼容新的notx修复版本", func(t *testing.T) {
|
t.Run("120多个历史checksum都可兼容新的notx修复版本", func(t *testing.T) {
|
||||||
for _, dbChecksum := range []string{
|
for _, dbChecksum := range []string{
|
||||||
"e77921f79d539bc24575cb9c16cbe566d2b23ce816190343d0a7568f6a3fcf61",
|
"e77921f79d539bc24575cb9c16cbe566d2b23ce816190343d0a7568f6a3fcf61",
|
||||||
|
|||||||
@@ -96,6 +96,9 @@ func TestIsMigrationChecksumCompatible_AdditionalCases(t *testing.T) {
|
|||||||
|
|
||||||
func TestMigrationChecksumCompatibilityRules_CoverEditedUpgradeCompatibilityMigrations(t *testing.T) {
|
func TestMigrationChecksumCompatibilityRules_CoverEditedUpgradeCompatibilityMigrations(t *testing.T) {
|
||||||
for _, name := range []string{
|
for _, name := range []string{
|
||||||
|
"109_auth_identity_compat_backfill.sql",
|
||||||
|
"110_pending_auth_and_provider_default_grants.sql",
|
||||||
|
"112_add_payment_order_provider_key_snapshot.sql",
|
||||||
"115_auth_identity_legacy_external_backfill.sql",
|
"115_auth_identity_legacy_external_backfill.sql",
|
||||||
"116_auth_identity_legacy_external_safety_reports.sql",
|
"116_auth_identity_legacy_external_safety_reports.sql",
|
||||||
"118_wechat_dual_mode_and_auth_source_defaults.sql",
|
"118_wechat_dual_mode_and_auth_source_defaults.sql",
|
||||||
|
|||||||
@@ -158,8 +158,12 @@ func (s *PaymentService) checkPaid(ctx context.Context, o *dbent.PaymentOrder) s
|
|||||||
"queryRef": queryRef,
|
"queryRef": queryRef,
|
||||||
})
|
})
|
||||||
slog.Warn("query upstream returned invalid paid amount", "orderID", o.ID, "queryRef", queryRef, "paid", resp.Amount)
|
slog.Warn("query upstream returned invalid paid amount", "orderID", o.ID, "queryRef", queryRef, "paid", resp.Amount)
|
||||||
|
retriedResp, retryOK := requeryPaidOrderOnce(ctx, prov, queryRef)
|
||||||
|
if !retryOK {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
resp = retriedResp
|
||||||
|
}
|
||||||
notificationTradeNo := o.PaymentTradeNo
|
notificationTradeNo := o.PaymentTradeNo
|
||||||
if upstreamTradeNo := strings.TrimSpace(resp.TradeNo); paymentOrderShouldPersistUpstreamTradeNo(queryRef, upstreamTradeNo, notificationTradeNo) {
|
if upstreamTradeNo := strings.TrimSpace(resp.TradeNo); paymentOrderShouldPersistUpstreamTradeNo(queryRef, upstreamTradeNo, notificationTradeNo) {
|
||||||
if _, updateErr := s.entClient.PaymentOrder.Update().
|
if _, updateErr := s.entClient.PaymentOrder.Update().
|
||||||
@@ -184,6 +188,21 @@ func (s *PaymentService) checkPaid(ctx context.Context, o *dbent.PaymentOrder) s
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func requeryPaidOrderOnce(ctx context.Context, prov payment.Provider, queryRef string) (*payment.QueryOrderResponse, bool) {
|
||||||
|
if prov == nil || strings.TrimSpace(queryRef) == "" {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
resp, err := prov.QueryOrder(ctx, queryRef)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn("query upstream retry failed", "queryRef", queryRef, "error", err)
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
if resp == nil || resp.Status != payment.ProviderStatusPaid || !isValidProviderAmount(resp.Amount) {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return resp, true
|
||||||
|
}
|
||||||
|
|
||||||
func paymentOrderQueryReference(order *dbent.PaymentOrder, prov payment.Provider) string {
|
func paymentOrderQueryReference(order *dbent.PaymentOrder, prov payment.Provider) string {
|
||||||
if order == nil {
|
if order == nil {
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ import (
|
|||||||
|
|
||||||
type paymentOrderLifecycleQueryProvider struct {
|
type paymentOrderLifecycleQueryProvider struct {
|
||||||
lastQueryTradeNo string
|
lastQueryTradeNo string
|
||||||
|
queryCalls int
|
||||||
|
responses []*payment.QueryOrderResponse
|
||||||
resp *payment.QueryOrderResponse
|
resp *payment.QueryOrderResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,6 +50,14 @@ func (p *paymentOrderLifecycleQueryProvider) CreatePayment(context.Context, paym
|
|||||||
|
|
||||||
func (p *paymentOrderLifecycleQueryProvider) QueryOrder(_ context.Context, tradeNo string) (*payment.QueryOrderResponse, error) {
|
func (p *paymentOrderLifecycleQueryProvider) QueryOrder(_ context.Context, tradeNo string) (*payment.QueryOrderResponse, error) {
|
||||||
p.lastQueryTradeNo = tradeNo
|
p.lastQueryTradeNo = tradeNo
|
||||||
|
p.queryCalls++
|
||||||
|
if len(p.responses) > 0 {
|
||||||
|
resp := p.responses[0]
|
||||||
|
if len(p.responses) > 1 {
|
||||||
|
p.responses = p.responses[1:]
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
return p.resp, nil
|
return p.resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,6 +244,103 @@ func TestVerifyOrderByOutTradeNoBackfillsTradeNoFromPaidQuery(t *testing.T) {
|
|||||||
require.Equal(t, user.ID, redeemRepo.useCalls[0].userID)
|
require.Equal(t, user.ID, redeemRepo.useCalls[0].userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestVerifyOrderByOutTradeNoRetriesZeroAmountPaidQueryOnce(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
client := newPaymentOrderLifecycleTestClient(t)
|
||||||
|
|
||||||
|
user, err := client.User.Create().
|
||||||
|
SetEmail("checkpaid-retry@example.com").
|
||||||
|
SetPasswordHash("hash").
|
||||||
|
SetUsername("checkpaid-retry-user").
|
||||||
|
Save(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
order, err := client.PaymentOrder.Create().
|
||||||
|
SetUserID(user.ID).
|
||||||
|
SetUserEmail(user.Email).
|
||||||
|
SetUserName(user.Username).
|
||||||
|
SetAmount(88).
|
||||||
|
SetPayAmount(88).
|
||||||
|
SetFeeRate(0).
|
||||||
|
SetRechargeCode("CHECKPAID-UPSTREAM-RETRY").
|
||||||
|
SetOutTradeNo("sub2_checkpaid_retry_zero_amount").
|
||||||
|
SetPaymentType(payment.TypeAlipay).
|
||||||
|
SetPaymentTradeNo("").
|
||||||
|
SetOrderType(payment.OrderTypeBalance).
|
||||||
|
SetStatus(OrderStatusPending).
|
||||||
|
SetExpiresAt(time.Now().Add(time.Hour)).
|
||||||
|
SetClientIP("127.0.0.1").
|
||||||
|
SetSrcHost("api.example.com").
|
||||||
|
Save(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
userRepo := &mockUserRepo{
|
||||||
|
getByIDUser: &User{
|
||||||
|
ID: user.ID,
|
||||||
|
Email: user.Email,
|
||||||
|
Username: user.Username,
|
||||||
|
Balance: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
userRepo.updateBalanceFn = func(ctx context.Context, id int64, amount float64) error {
|
||||||
|
require.Equal(t, user.ID, id)
|
||||||
|
if userRepo.getByIDUser != nil {
|
||||||
|
userRepo.getByIDUser.Balance += amount
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
redeemRepo := &paymentOrderLifecycleRedeemRepo{
|
||||||
|
codesByCode: map[string]*RedeemCode{
|
||||||
|
order.RechargeCode: {
|
||||||
|
ID: 1,
|
||||||
|
Code: order.RechargeCode,
|
||||||
|
Type: RedeemTypeBalance,
|
||||||
|
Value: order.Amount,
|
||||||
|
Status: StatusUnused,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
redeemService := NewRedeemService(
|
||||||
|
redeemRepo,
|
||||||
|
userRepo,
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
client,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
registry := payment.NewRegistry()
|
||||||
|
provider := &paymentOrderLifecycleQueryProvider{
|
||||||
|
responses: []*payment.QueryOrderResponse{
|
||||||
|
{
|
||||||
|
TradeNo: "upstream-trade-zero",
|
||||||
|
Status: payment.ProviderStatusPaid,
|
||||||
|
Amount: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
TradeNo: "upstream-trade-retry",
|
||||||
|
Status: payment.ProviderStatusPaid,
|
||||||
|
Amount: 88,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
registry.Register(provider)
|
||||||
|
|
||||||
|
svc := &PaymentService{
|
||||||
|
entClient: client,
|
||||||
|
registry: registry,
|
||||||
|
redeemService: redeemService,
|
||||||
|
userRepo: userRepo,
|
||||||
|
providersLoaded: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := svc.VerifyOrderByOutTradeNo(ctx, order.OutTradeNo, user.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 2, provider.queryCalls)
|
||||||
|
require.Equal(t, OrderStatusCompleted, got.Status)
|
||||||
|
require.Equal(t, "upstream-trade-retry", got.PaymentTradeNo)
|
||||||
|
}
|
||||||
|
|
||||||
func TestVerifyOrderByOutTradeNoRejectsPaidQueryWithZeroAmount(t *testing.T) {
|
func TestVerifyOrderByOutTradeNoRejectsPaidQueryWithZeroAmount(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
client := newPaymentOrderLifecycleTestClient(t)
|
client := newPaymentOrderLifecycleTestClient(t)
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
-- Preserve legacy OIDC behavior for upgraded installs that predate the
|
||||||
|
-- introduction of secure PKCE/id_token defaults. Fresh installs continue to
|
||||||
|
-- inherit runtime defaults when these rows are absent.
|
||||||
|
|
||||||
|
WITH legacy_oidc_install AS (
|
||||||
|
SELECT 1
|
||||||
|
FROM settings
|
||||||
|
WHERE key IN (
|
||||||
|
'oidc_connect_enabled',
|
||||||
|
'oidc_connect_client_id',
|
||||||
|
'oidc_connect_authorize_url',
|
||||||
|
'oidc_connect_token_url',
|
||||||
|
'oidc_connect_issuer_url',
|
||||||
|
'oidc_connect_userinfo_url',
|
||||||
|
'oidc_connect_frontend_redirect_url'
|
||||||
|
)
|
||||||
|
LIMIT 1
|
||||||
|
)
|
||||||
|
INSERT INTO settings (key, value)
|
||||||
|
SELECT defaults.key, 'false'
|
||||||
|
FROM legacy_oidc_install
|
||||||
|
CROSS JOIN (
|
||||||
|
VALUES
|
||||||
|
('oidc_connect_use_pkce'),
|
||||||
|
('oidc_connect_validate_id_token')
|
||||||
|
) AS defaults(key)
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM settings existing
|
||||||
|
WHERE existing.key = defaults.key
|
||||||
|
)
|
||||||
|
ON CONFLICT (key) DO NOTHING;
|
||||||
@@ -115,3 +115,15 @@ func TestMigration123BackfillsLegacyAuthSourceGrantDefaultsSafely(t *testing.T)
|
|||||||
require.Contains(t, sql, "value = 'false'")
|
require.Contains(t, sql, "value = 'false'")
|
||||||
require.Contains(t, sql, "auth_identity_migration_reports")
|
require.Contains(t, sql, "auth_identity_migration_reports")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMigration124BackfillsLegacyOIDCSecurityFlagsSafely(t *testing.T) {
|
||||||
|
content, err := FS.ReadFile("124_backfill_legacy_oidc_security_flags.sql")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
sql := string(content)
|
||||||
|
require.Contains(t, sql, "oidc_connect_use_pkce")
|
||||||
|
require.Contains(t, sql, "oidc_connect_validate_id_token")
|
||||||
|
require.Contains(t, sql, "ON CONFLICT (key) DO NOTHING")
|
||||||
|
require.Contains(t, sql, "oidc_connect_enabled")
|
||||||
|
require.Contains(t, sql, "'false'")
|
||||||
|
}
|
||||||
|
|||||||
@@ -841,7 +841,7 @@ linuxdo_connect:
|
|||||||
frontend_redirect_url: "/auth/linuxdo/callback"
|
frontend_redirect_url: "/auth/linuxdo/callback"
|
||||||
token_auth_method: "client_secret_post" # client_secret_post | client_secret_basic | none
|
token_auth_method: "client_secret_post" # client_secret_post | client_secret_basic | none
|
||||||
# 注意:当 token_auth_method=none(public client)时,必须启用 PKCE
|
# 注意:当 token_auth_method=none(public client)时,必须启用 PKCE
|
||||||
use_pkce: false
|
use_pkce: true
|
||||||
userinfo_email_path: ""
|
userinfo_email_path: ""
|
||||||
userinfo_id_path: ""
|
userinfo_id_path: ""
|
||||||
userinfo_username_path: ""
|
userinfo_username_path: ""
|
||||||
|
|||||||
Reference in New Issue
Block a user