PAYATHON 2026

Delphi 如何接收 Android Intent 支付回调

支付小周

结论

支付应用返回的是 deep link Intent。支付结果通常保存在 Intent.getData 表示的 URI 中,而不在 Intent.getExtras 里。

所以下面的读取方式不适用于这种回调:

Extras := Data.getExtras;
Extras.getString(TJIntent.JavaClass.EXTRA_TEXT);

EXTRA_TEXT 主要用于文本分享等 Intent。deep link 回调通常类似:

scheme_return://pay-response?参数1=值1&参数2=值2

应通过 Data.getData 获取 URI,再用 getQueryParameter 读取结果。

正确读取回调 URI

可以把 HandleIntentAction 改成下面这样:

uses
  System.SysUtils,
  Androidapi.JNI.GraphicsContentViewText,
  Androidapi.JNI.Net,
  Androidapi.Helpers;

function TForm1.HandleIntentAction(const Data: JIntent): Boolean;
var
  CallbackUri: Jnet_Uri;
  RawUri: string;
  Scheme: string;
  Host: string;
begin
  Result := False;

  if Data = nil then
    Exit;

  if not TJIntent.JavaClass.ACTION_VIEW.equals(Data.getAction) then
    Exit;

  CallbackUri := Data.getData;
  if CallbackUri = nil then
    Exit;

  RawUri := JStringToString(CallbackUri.toString);
  Scheme := JStringToString(CallbackUri.getScheme);
  Host := JStringToString(CallbackUri.getHost);

  Memo1.Lines.BeginUpdate;
  try
    Memo1.Clear;
    Memo1.Lines.Add('Raw URI: ' + RawUri);
    Memo1.Lines.Add('Scheme: ' + Scheme);
    Memo1.Lines.Add('Host: ' + Host);

    { 使用支付 SDK 实际规定的参数名读取,例如:
    Memo1.Lines.Add(
      'status: ' +
      JStringToString(
        CallbackUri.getQueryParameter(StringToJString('status'))
      )
    );
    }
  finally
    Memo1.Lines.EndUpdate;
  end;

  Result := True;
end;

先输出完整的 Raw URI,查看实际收到的地址。然后根据 Stone SDK 对回调字段的定义,使用相应的参数名读取:

Value := CallbackUri.getQueryParameter(
  StringToJString('SDK规定的参数名')
);

在看到实际回调地址之前,不要假定参数名一定是 status、transaction_id 或其他名称。

如果只是想确认应用是否收到了 deep link,也可以直接读取:

if Data.getDataString <> nil then
  Memo1.Text := JStringToString(Data.getDataString);

Manifest 必须与回调地址一致

当前配置只能匹配这种结构的地址:

scheme_return://pay-response/...

对应的配置是:

<intent-filter>
    <action android:name="android.intent.action.VIEW" />

    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />

    <data
        android:scheme="scheme_return"
        android:host="pay-response" />
</intent-filter>

这里的 scheme_return 对应 URI 的 scheme,pay-response 对应 URI 的 host。两者都必须与支付应用实际返回的 URI 一致。

例如,下面的地址可以匹配上述过滤器:

scheme_return://pay-response?result=success

下面这个地址则不一定能匹配:

scheme_return://payment-result?result=success

因为它的 host 是 payment-result。

如果支付 SDK 只允许指定 return_scheme,而回调 host 由 SDK 固定生成,就要按照 SDK 实际生成的 host 配置 Manifest,不能只根据发送请求时使用的参数推断 host。

不要编码整个请求 URI

发送代码中还有一个潜在问题:

TIdURI.URLEncode(uri)

不要对完整 URI 进行 URL 编码,否则 :、/、?、& 等结构字符可能也会被编码,进而改变 URI 的含义。完整 URI 应直接解析,只对各个参数值单独编码。

如果 valorTotal 只包含 SDK 要求的数字格式,可以先这样写:

var
  Uri: string;
  Intent: JIntent;
begin
  Uri :=
    'payment-app://pay' +
    '?return_scheme=scheme_return' +
    '&amount=' + valorTotal +
    '&transaction_type=debit';

  Intent := TJIntent.JavaClass.init(
    TJIntent.JavaClass.ACTION_VIEW,
    TJnet_Uri.JavaClass.parse(StringToJString(Uri))
  );

  SharedActivity.startActivity(Intent);
end;

更稳妥的方式是使用 Uri.Builder 构造查询参数,让 Android API 负责参数值的编码:

var
  PaymentUri: Jnet_Uri;
  Intent: JIntent;
begin
  PaymentUri :=
    TJnet_Uri.JavaClass.parse(
      StringToJString('payment-app://pay')
    ).buildUpon
      .appendQueryParameter(
        StringToJString('return_scheme'),
        StringToJString('scheme_return')
      )
      .appendQueryParameter(
        StringToJString('amount'),
        StringToJString(valorTotal)
      )
      .appendQueryParameter(
        StringToJString('transaction_type'),
        StringToJString('debit')
      )
      .build;

  Intent := TJIntent.JavaClass.init(
    TJIntent.JavaClass.ACTION_VIEW,
    PaymentUri
  );

  SharedActivity.startActivity(Intent);
end;

同时处理冷启动和已运行状态

Android 回调进入应用时有两种情况:

  • 应用尚未运行,回调 Intent 会成为启动 Intent。
  • 应用已经运行,Activity 会通过 onNewIntent 收到新的 Intent。

现有代码同时使用 MainActivity.getIntent 和 TMessageReceivedNotification,处理方向没有问题,可以保留这两条接收路径:

procedure TForm1.FormCreate(Sender: TObject);
var
  AppEventService: IFMXApplicationEventService;
begin
  if TPlatformServices.Current.SupportsPlatformService(
    IFMXApplicationEventService,
    AppEventService
  ) then
    AppEventService.SetApplicationEventHandler(HandleAppEvent);

  MainActivity.registerIntentAction(
    TJIntent.JavaClass.ACTION_VIEW
  );

  TMessageManager.DefaultManager.SubscribeToMessage(
    TMessageReceivedNotification,
    HandleActivityMessage
  );
end;

窗体销毁时要取消订阅,以免处理器被重复注册,或者继续访问已经释放的窗体:

procedure TForm1.FormDestroy(Sender: TObject);
begin
  TMessageManager.DefaultManager.Unsubscribe(
    TMessageReceivedNotification,
    HandleActivityMessage
  );
end;

应用恢复时读取启动 Intent:

function TForm1.HandleAppEvent(
  AAppEvent: TApplicationEvent;
  AContext: TObject
): Boolean;
var
  StartupIntent: JIntent;
begin
  Result := False;

  if AAppEvent <> TApplicationEvent.BecameActive then
    Exit;

  StartupIntent := MainActivity.getIntent;
  if StartupIntent <> nil then
    Result := HandleIntentAction(StartupIntent);
end;

收到新的 Intent 消息时直接处理:

procedure TForm1.HandleActivityMessage(
  const Sender: TObject;
  const M: TMessage
);
begin
  if M is TMessageReceivedNotification then
    HandleIntentAction(
      TMessageReceivedNotification(M).Value
    );
end;

避免重复处理同一个回调

应用多次进入前台时,MainActivity.getIntent 可能一直返回同一个 Intent,导致同一笔支付结果被重复处理。业务层需要根据支付流水号等字段进行幂等判断。

处理完成后,也可以清除当前 Activity 保存的 Intent:

MainActivity.setIntent(
  TJIntent.JavaClass.init
);

是否清除要根据应用的生命周期设计决定。即使已经清除 Intent,仍要通过订单号或交易号对支付结果去重,不能只依赖界面状态。

为什么之前始终得到 nil

应用并不是还在“等待 response”,问题在于读取位置不对:

Data.getExtras

这里读取的是 Intent 附加数据,而 deep link 的主要内容在:

Data.getData

可以按以下顺序排查:

  1. 确认 Data 不为 nil。
  2. 确认 Data.getAction 是 ACTION_VIEW。
  3. 确认 Data.getData 不为 nil。
  4. 输出 Data.getDataString,查看完整的回调地址。
  5. 校验 scheme 和 host。
  6. 按照 SDK 定义的参数名调用 getQueryParameter。
  7. 对支付结果进行签名校验和幂等处理。

如果 HandleIntentAction 已经被调用,但 Data.getData 仍然是 nil,就需要记录 Data.getAction、Data.getType 和 extras 的键名,确认支付应用实际采用的是 deep link、extras,还是其他返回协议。按照当前 Manifest 中的 ACTION_VIEW 和 URI 配置,应优先从 Intent.getData 读取。

备注:内容仅供参考。