直连-本地支付
网关地址
POST
https://sandbox.j-pay.net/pay_index
测试账号
Merchant ID:10172
ApiKey: prx5d81g2ytb1mmokl9mohjsmsb09do3
请求参数
| 参数名称 | 类型 | 是否必填 | 参与签名 | 参数说明 |
|---|---|---|---|---|
| pay_memberid | String | 是 | 是 | 平台分配商户号 |
| pay_orderid | String | 是 | 是 | 上送订单号唯一, 字符长度 20 |
| pay_applydate | String | 是 | 是 | 提交时间, 时间格式:2016-12-26 18:18:18 |
| pay_bankcode | String | 是 | 是 | 银行编码, 参考后续说明:901 |
| pay_notifyurl | String | 是 | 是 | 服务端返回地址.(POST 返回数据) |
| pay_callbackurl | String | 是 | 是 | 页面跳转返回地址(POST 返回数据) |
| pay_amount | String | 是 | 是 | 商品金额 |
| pay_md5sign | String | 是 | 否 | 请看 MD5 签名字段格式 |
| pay_currency | String | 是 | 否 | 币种代码,大写字母 (USD) |
| pay_url | String | 是 | 否 | 网站URL, 需提交审核、带http(s):// |
| pay_type | String | 是 | 否 | 本地支付固定值:apm |
| pay_method | String | 否 | 否 | 支付 方式列表,不传则跳转收银台 如 alipay |
| pay_app_scheme | String | 否 | 否 | APP url scheme, 不为空时优先同步返回, 如: jpayapm://payment/result |
+pay_productname | String | 否 | 否 | 商品信息,参数由下列一组数据,通过JSON格式组成,注意参数名大小写敏感。本字段以JSON数组字符串来填值。示例:[{"sku":"123456789","productName":"MacBook Pro","productImage":"产品图片地址","attributes":"Size:US8 Color:blue","price":"11000.00","quantity":"1"},{"sku":"9876543231","productName":"IPhone 12","productImage":"产品图片地址","attributes":"Size:US8 Color:blue","price":"11000.00","quantity":"1"}] |
| 「sku | String | 否 | 否 | 商品编号 (唯一) |
| 「productName | String | 是 | 否 | 产品名称 |
| 「productImage | String | 否 | 否 | 产品图片URL |
| 「attributes | String | 否 | 否 | 产品属性 |
| 「price | String | 是 | 否 | 产品价格 |
| 「quantity | String | 是 | 否 | 产品数量 |
| pay_firstname | String | 是 | 否 | 账单名字 |
| pay_lastname | String | 是 | 否 | 账单姓氏 |
| pay_street_address1 | String | 否 | 否 | 账单地址1 |
| pay_street_address2 | String | 否 | 否 | 账单地址2 |
| pay_city | String | 否 | 否 | 账单城市 |
| pay_postcode | String | 否 | 否 | 账单邮编 |
| pay_state | String | 否 | 否 | 账单州/省 |
| pay_country_iso_code_2 | String | 是 | 否 | 账单国家 |
| pay_email_address | String | 是 | 否 | 账单邮箱 |
| pay_telephone | String | 是 | 否 | 账单电话 |
| shipping_firstname | String | 否 | 否 | 收件名字 |
| shipping_lastname | String | 否 | 否 | 收件姓氏 |
| shipping_street_address1 | String | 否 | 否 | 收件地址1 |
| shipping_street_address2 | String | 否 | 否 | 收件地址2 |
| shipping_city | String | 否 | 否 | 收件城市 |
| shipping_state | String | 否 | 否 | 收件州/省 |
| shipping_postcode | String | 否 | 否 | 收件邮编 |
| shipping_country_iso_code_2 | String | 否 | 否 | 收件国家 |
| shipping_telephone | String | 否 | 否 | 收件电话 |
| pay_ip | String | 是 | 否 | 用户IP地址 |
| pay_useragent | String | 是 | 否 | 用户浏览器信息 |
| pay_language | String | 是 | 否 | 语言 |
| system | String | 是 | 否 | 网站系统, 如:Shopyy/Shopline |
请求支付返回参数
| 参数 名称 | 类型 | 参数说明 |
|---|---|---|
| status | Int | status = 0:SUCCESS status = 1:3d支付跳转 status = 2:failed |
| msg | String | 下单成功 or 下单失败 |
| url | String | 3d支付跳转地址 |
返回示例
Transaction successful : {
"status": 0,
"msg": "transaction success"
}
Transaction failed : {
"status": 2,
"msg": "transaction failed"
}
Redirect/3ds payment successful : {
"status": 1,
"url": "redirectUrl"
}
3DS验证
跳转支付和直接信用卡支付需要3DS验证
同步返回:(application/x-www-form-urlencoded)
| 参数名称 | 类型 | 是否必填 | 参数说明 |
|---|---|---|---|
| paymentStatus | String | 是 | Succeeded: 成功 Fail:失败 Processing:处理中 |
| orderID | String | 是 | 订单号 |
示例
同步返回:
https://www.xxxx.com/callback.html?paymentStatus=succeeded&orderID=1001
异步返回:(application/x-www-form-urlencoded)
| 参数名称 | 类型 | 是否必填 | 参数说明 |
|---|---|---|---|
| memberid | String | 是 | 商户 ID |
| orderid | String | 是 | 商户订单号 |
| amount | String | 是 | 提交的订单金额 |
| true_amount | String | 是 | 买家实际付款的金额 |
| currency | String | 是 | 币种 |
| transaction_id | String | 是 | 平台订单号 |
| returncode | String | 是 | “00” 为成功 "2":为失败 |
| datetime | String | 是 | 交易时间,格式:20220419000114 |
| sign | String | 否 | 请看验证签名字段格式 |
| attach | String |
Array{
["memberid"]=>
string(5) "10012"
["orderid"]=>
string(7) "7758292"
["transaction_id"]=>
string(20) "20230117104641499957"
["amount"]=>
string(5) "85.95"
["true_amount"]=>
string(5) "85.95"
["currency"]=>
string(5) "USD"
["datetime"]=>
string(14) "20230117111809"
["returncode"]=>
string(2) "00"
["sign"]=>
string(32) "48CF5D99A2ADE25DCEF73BC1B1136B9E"
["attach"]=>
string(0) ""
}
Demo
- Javascript
- Php
- Python
import { md5 } from "js-md5";
import axios from "axios";
const KEY = "prx5d81g2ytb1mmokl9mohjsmsb09do3";
const MEMBER_ID = "10172";
let query = {
pay_memberid: MEMBER_ID,
pay_orderid: "O" + Date.now().valueOf(),
pay_applydate: "2024-12-02 16:48:42",
pay_bankcode: "901",
pay_notifyurl: "https://www.google.com",
pay_callbackurl: "https://www.google.com",
pay_amount: "10.00",
};
let signData = [];
Object.keys(query)
.sort()
.forEach((key) => signData.push(`${key}=${query[key]}`));
signData.push(`key=${KEY}`);
Object.assign(query, {
pay_md5sign: md5(signData.join("&")).toUpperCase(),
pay_url: "https://www.google.com",
pay_currency: "USD",
pay_firstname: "Rodrigo",
pay_lastname: "lopez",
pay_country_iso_code_2: "CL",
pay_email_address: "[email protected]",
pay_telephone: "920508435",
pay_ip: "186.156.221.158",
pay_useragent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36 Edg/111.0.1661.43",
pay_language: "en",
system: "shopyy",
});
axios
.request({
url: "https://sandbox.j-pay.net/pay_index",
method: "post",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
data: query,
})
.then(({ data }) => {
console.log("success", data);
})
.catch((error) => {
console.log("error", error);
});
$url = 'https://sandbox.j-pay.net/pay_index';
$data = [
'pay_memberid' => '10172',
'pay_orderid' => 'O' . date('YmdHis', time()),
'pay_applydate' => date('Y-m-d H:i:s', time()),
'pay_bankcode' => '901',
'pay_notifyurl' => 'https://www.google.com',
'pay_callbackurl' => 'https://www.google.com',
'pay_amount' => '10.00',
];
ksort($data);
$md5str = "";
foreach ($data as $key => $val) {
if (!empty($val)) {
$md5str = $md5str . $key . "=" . $val . "&";
}
}
$sign_str = $md5str . 'key=prx5d81g2ytb1mmokl9mohjsmsb09do3';
$sign = strtoupper(md5($sign_str));
$data = array_merge($data, [
'pay_md5sign' => $sign,
'pay_url' => 'https://www.google.com',
'pay_currency' => 'USD',
'pay_firstname' => 'Rodrigo',
'pay_lastname' => 'lopez',
'pay_country_iso_code_2' => 'CL',
'pay_email_address' => '[email protected]',
'pay_telephone' => '920508435',
'pay_ip' => '186.156.221.158',
'pay_useragent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36 Edg/111.0.1661.43',
'pay_language' => 'es',
'system' => 'shopyy'
]);
$header = [
'Content-Type: application/x-www-form-urlencoded',
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER , false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST,false);
$data = curl_exec($ch);
if (curl_error($ch)) {
echo 'Curl error: ' . curl_error($ch);
}
curl_close($ch);
var_dump($data);
import hashlib
import requests
import time
import json
url = 'https://sandbox.j-pay.net/pay_index'
key = 'prx5d81g2ytb1mmokl9mohjsmsb09do3'
params = {
'pay_memberid': '10172',
'pay_orderid': 'O' + time.strftime('%Y%m%d%H%M%S', time.localtime()),
'pay_applydate': time.strftime('%Y%m%d%H%M%S', time.localtime()),
'pay_bankcode': '901',
'pay_notifyurl': 'http://www.google.com',
'pay_callbackurl': 'http://www.google.com',
'pay_amount': '10.00',
}
strParam = ''
for p in sorted(params):
strParam = strParam + str(p) + "=" + str(params[p]) + '&'
strParam = strParam + 'key' + '=' + key
parmStr = strParam.encode("utf-8")
m = hashlib.md5()
m.update(parmStr)
params['pay_md5sign'] = m.hexdigest().upper()
otherParms = {
"pay_url": "http://www.google.com",
"pay_currency": "USD",
"pay_firstname": "Rodrigo",
"pay_lastname": "lopez",
"pay_country_iso_code_2": "CL",
"pay_email_address": "[email protected]",
"pay_telephone": "920508435",
"pay_ip": "186.156.221.158",
"pay_useragent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36 Edg/111.0.1661.43",
"pay_language": "es",
"system": "shopyy"
}
res = requests.post(url = url, data = dict(params, **otherParms))
print('response:' + res.text)
App Demo
- Kotlin
- Swift
- Uniapp
接口新增参数示例:
pay_app_scheme: jpayapm://payment/result
Demo下载:
添加(或更新)构建依赖项
dependencies {
implementation "androidx.browser:browser:1.8.0"
}
JPayAPM.kt
package com.jpay.apm
import android.app.Activity
import android.content.Context
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.browser.customtabs.*
import org.json.JSONObject
import java.lang.ref.WeakReference
/**
* JPay APM
*/
class JPayAPM {
companion object {
const val PAY_STATUS_UNPAID = 0
const val PAY_STATUS_SUCCESS = 1
const val PAY_STATUS_FAILED = 2
const val PAY_STATUS_CANCEL = 3
private const val TAG = "JPayAPM"
}
private var mActivityRef: WeakReference<Activity>? = null
private var mContextRef: WeakReference<Context>? = null
private var payStatus: Int = PAY_STATUS_UNPAID
private var payCallback: WeakReference<((JSONObject) -> Unit)?> = WeakReference(null)
constructor(activity: Activity, context: Context) {
mActivityRef = WeakReference(activity)
mContextRef = WeakReference(context)
}
/**
* Execute payment
* @param payMethod Payment method (only support googlepay)
* @param payUrl Payment URL
* @param callback Result callback
*/
fun exec(payMethod: String, payUrl: String, callback: ((JSONObject) -> Unit)?) {
Log.d(TAG, "Execute payment: method=$payMethod, url=$payUrl")
payStatus = PAY_STATUS_UNPAID
payCallback = WeakReference(callback)
payByUrl(payUrl)
}
/**
* Handle payment result (main thread callback)
*/
private fun handlePayResult(status: Int, message: String, orderId: String = "") {
payStatus = status
Log.d(TAG, "Payment result: status=$status, msg=$message, orderId=$orderId")
// Build JSON result
val result = JSONObject().apply {
put("status", status)
put("message", message)
put("orderId", orderId)
}
Handler(Looper.getMainLooper()).post {
payCallback.get()?.invoke(result)
payCallback.clear()
}
}
/**
* Open payment URL
*/
private fun payByUrl(payUrl: String) {
val activity = mActivityRef?.get()
val context = mContextRef?.get()
if (context == null) {
handlePayResult(PAY_STATUS_FAILED, "Context is null", "")
return
}
if (activity == null || activity.isFinishing ||
(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && activity.isDestroyed)) {
handlePayResult(PAY_STATUS_FAILED, "Activity is null or destroyed", "")
return
}
val uri: Uri
try {
uri = Uri.parse(payUrl)
if (uri.scheme.isNullOrEmpty() || uri.host.isNullOrEmpty()) {
handlePayResult(PAY_STATUS_FAILED, "Invalid URL format: $payUrl", "")
return
}
} catch (e: Exception) {
handlePayResult(PAY_STATUS_FAILED, "Failed to parse URL: ${e.message}", "")
return
}
try {
val intent = CustomTabsIntent.Builder()
.setShowTitle(true)
.build()
intent.launchUrl(activity, uri)
} catch (e: Exception) {
Log.e(TAG, "Failed to launch payment URL: $payUrl", e)
handlePayResult(PAY_STATUS_FAILED, "Failed to launch payment URL: ${e.message}", "")
}
}
/**
* Trigger payment result (for Scheme callback)
*/
fun triggerPayResult(status: Int, message: String, orderId: String = "") {
handlePayResult(status, message, orderId)
}
/**
* Release resources
*/
fun release() {
mActivityRef?.clear()
mContextRef?.clear()
payCallback.clear()
payStatus = PAY_STATUS_UNPAID
}
}
方案示例
import Foundation
import UIKit
import SafariServices
enum PaymentStatus: Int {
case unpaid = 0
case succeeded = 1
case failed = 2
}
typealias PaymentCallback = (Int, String, String) -> Void
extension UIViewController {
static func topViewController() -> UIViewController {
guard let windowScene = UIApplication.shared.connectedScenes
.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene,
let keyWindow = windowScene.windows.first(where: { $0.isKeyWindow }),
var topVC = keyWindow.rootViewController else {
return UIViewController()
}
while let presentedVC = topVC.presentedViewController {
topVC = presentedVC
}
return topVC
}
}
@objcMembers class JPayAPM: NSObject, SFSafariViewControllerDelegate {
private var payStatus: PaymentStatus = .unpaid
private var isCallbackCalled: Bool = false
private var paymentCallback: PaymentCallback? = nil
private var safariViewController: SFSafariViewController?
/// Execute payment request
/// - Parameters:
/// - payMethod: Payment method identifier
/// - url: Payment URL
/// - callback: Callback function
public func exec(_ payMethod: String, _ url: String, _ callback: @escaping ([String: Any]) -> Void) {
// print("Execute payment request: payMethod=\(payMethod), url=\(url)")
self.payStatus = .unpaid
self.isCallbackCalled = false
self.paymentCallback = { status, message, orderId in
let result: [String: Any] = [
"status": status,
"message": message,
"orderId": orderId
]
callback(result)
}
guard let paymentUrl = URL(string: url) else {
let errorMsg = "Invalid URL format: \(url)"
print(errorMsg)
let result: [String: Any] = [
"status": PaymentStatus.failed.rawValue,
"message": errorMsg,
"orderId": ""
]
callback(result)
return
}
DispatchQueue.main.async {
self.showPaymentBrowser(with: paymentUrl)
}
}
private func showPaymentBrowser(with url: URL) {
let configuration = SFSafariViewController.Configuration()
configuration.barCollapsingEnabled = true
let safariVC = SFSafariViewController(url: url, configuration: configuration)
safariVC.dismissButtonStyle = .done
safariVC.delegate = self
self.safariViewController = safariVC
let currentVC = UIViewController.topViewController()
currentVC.present(safariVC, animated: true) {
print("SafariViewController presented, URL: \(url.absoluteString)")
}
}
private func notifyPaymentResult(status: PaymentStatus, message: String, orderID: String) {
guard !self.isCallbackCalled else { return }
self.isCallbackCalled = true
self.payStatus = status
if let callback = self.paymentCallback {
callback(status.rawValue, message, orderID)
}
}
private func parsePaymentResult(from url: URL, controller: SFSafariViewController) {
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return }
let queryItems = components.queryItems ?? []
let paymentStatus = queryItems.first(where: { $0.name == "paymentStatus" })?.value?.lowercased()
let orderID = queryItems.first(where: { $0.name == "orderID" })?.value ?? ""
if let status = paymentStatus {
let resultStatus: PaymentStatus = status == "succeeded" ? .succeeded : .failed
let resultMsg = status == "succeeded" ? "Payment succeeded" : "Payment failed"
notifyPaymentResult(status: resultStatus, message: resultMsg, orderID: orderID)
controller.dismiss(animated: true) {
self.safariViewController = nil
}
}
}
func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
print("SafariViewController dismissed")
notifyPaymentResult(status: .failed, message: "User closed the payment page", orderID: "")
self.safariViewController = nil
}
func safariViewController(_ controller: SFSafariViewController, initialLoadDidRedirectTo url: URL) {
print("SafariViewController redirect to: \(url.absoluteString)")
parsePaymentResult(from: url, controller: controller)
}
func safariViewController(_ controller: SFSafariViewController, didCompleteInitialLoad didLoadSuccessfully: Bool) {
if didLoadSuccessfully {
print("SafariViewController initial load successfully")
} else {
print("SafariViewController initial load failed")
notifyPaymentResult(status: .failed, message: "Payment page load failed", orderID: "")
controller.dismiss(animated: true)
}
}
}
下载插件并解压到uni_modules目录
使用
import { JPay } from '@/uni_modules/jpay-apm'
JPay({
payMethod: 'applepay', //Payment method
url: [The url returned from placing an order through the interface]
success: (res) => {
console.log('Payment successful', res)
},
fail: (res) => {
console.log('Payment failed', res.message)
},
complete: (res) => {
console.log('Payment complete', res)
}
})
App.vue
<script>
export default {
onShow: function() {
const schemeUrl = plus?.runtime?.arguments;
const params = this.parseSchemeParams(schemeUrl);
if (params?.orderID) {
console.log('You can jump to the result page');
}
},
methods: {
parseSchemeParams(schemeUrl) {
if (!schemeUrl) return {};
const paramStr = schemeUrl.split('?')[1] || '';
if (!paramStr) return {};
const params = {};
paramStr.split('&').forEach(item => {
const [key, value] = item.split('=');
if (key) {
params[key] = decodeURIComponent(value || '');
}
});
return params;
}
}
}
</script>