真 · 保姆级支付宝沙盒支付教程(本人遇坑多次总结)

一 .前置工作
        1.首先,前置工作肯定要先搭建支付宝沙盒环境。打开支付宝的开发者平台,地址:支付宝开放平台,先登录账号,如下图

2.接下来就点击旁边的“控制台”,进入控制台页面,然后鼠标滚轮滑倒最下面,下图所示

点击“开发工具推荐”中的沙箱,进入沙箱调试页面,下图所示

接下来

然后就要下载生成密钥的工具,安装软件就不用兄弟教学了吧,你可以的

3.然后打开“支付宝开放平台密钥工具”,点击生成密钥

最后生成页面

回到支付宝开放平台页面操作,复制公钥进去点击保存

4.然后下载支付宝沙盒版本去到要操作的设备中去,然后登录

最后打开沙箱版支付宝,登录买家账号,注意:一定要登录买家账号

5.到这一步,前置工作已经全部完成,已经离成功不远了
二.代码层面
1.创建一个新的项目(这个就不用教了)

我这里就只做一个简单得页面算了,上代码!

创建项目之后在app模块中的build.gradle中加入支付的依赖

  implementation 'com.alipay.sdk:alipaysdk-android:+@aar'
2.布局页面  activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/rc_view"/>

</androidx.constraintlayout.widget.ConstraintLayout>
3.接下来左一个列表视图需要的适配器 RcAdapter 
public class RcAdapter extends RecyclerView.Adapter<RcAdapter.RcViewHolder> {

    private List<RcBean> list = new ArrayList<>();
    
    public SetItemOnClickListener listener;

    public RcAdapter( List<RcBean> list) {
        this.list = list;
    }

    @Override
    public RcViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        return new RcViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_rc, parent, false));
    }

    @Override
    public void onBindViewHolder(RcViewHolder holder, int position) {
        holder.tv_name.setText(list.get(position).getName());
        holder.tv_money.setText(String.valueOf(list.get(position).getMoney()));
        holder.itemView.setOnClickListener(v -> {
            listener.setItemOnClickListener(position);
        });
    }

    @Override
    public int getItemCount() {
        return list.size();
    }

    class RcViewHolder extends RecyclerView.ViewHolder {

        TextView tv_name, tv_money;

        public RcViewHolder(View v) {
            super(v);
            tv_name = v.findViewById(R.id.tv_name);
            tv_money = v.findViewById(R.id.tv_money);
        }
    }
    
    public void setOnClickListener(SetItemOnClickListener listener) {
        this.listener = listener;
    }
    
    public interface SetItemOnClickListener {
        void setItemOnClickListener(int position);
        
    }

}

4.对应的数据类 RcBean
public class RcBean {

    private String name;

    private BigDecimal money;

    public RcBean(String name, BigDecimal money) {
        this.name = name;
        this.money = money;
    }

    public RcBean() {
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public BigDecimal getMoney() {
        return money;
    }

    public void setMoney(BigDecimal money) {
        this.money = money;
    }
}
5.列表 item对应的布局
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <LinearLayout
        android:id="@+id/l1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="10dp"
        android:orientation="vertical"
        app:layout_constraintTop_toTopOf="parent">

        <TextView
            android:id="@+id/tv_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="name"/>

        <TextView
            android:id="@+id/tv_money"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="money"/>

    </LinearLayout>


    <View
        android:layout_width="match_parent"
        android:layout_height="1dp"
        app:layout_constraintTop_toBottomOf="@id/l1"
        android:layout_marginTop="5dp"
        android:background="@color/black"/>


</androidx.constraintlayout.widget.ConstraintLayout>
6.然后复制官方给的demo中的三个类

第一个  OrderInfoUtil2_0

public class OrderInfoUtil2_0 {

	/**
	 * 构造授权参数列表
	 *
	 * @param pid
	 * @param app_id
	 * @param target_id
	 * @return
	 */
	public static Map<String, String> buildAuthInfoMap(String pid, String app_id, String target_id, String orderInfo,boolean rsa2) {

		SnowflakeIdGenerator snowflakeIdGenerator = new SnowflakeIdGenerator(1);

		Map<String, String> keyValues = new HashMap<String, String>();

		// 商户签约拿到的app_id,如:2013081700024223
		keyValues.put("app_id", app_id);

		// 商户签约拿到的pid,如:2088102123816631
		keyValues.put("pid", pid);

		// 服务接口名称, 固定值
		keyValues.put("apiname", "");

		// 服务接口名称, 固定值
		keyValues.put("methodname", "alipay.open.auth.sdk.code.get");

		// 商户类型标识, 固定值
		keyValues.put("app_name", "mc");

		// 业务类型, 固定值
		keyValues.put("biz_type", "openservice");

		// 产品码, 固定值
		keyValues.put("product_id", "APP_FAST_LOGIN");

		// 授权范围, 固定值
		keyValues.put("scope", "kuaijie");

		// 商户唯一标识,如:kkkkk091125
		keyValues.put("target_id", target_id);

		// 授权类型, 固定值
		keyValues.put("auth_type", "AUTHACCOUNT");

		// 签名类型
		keyValues.put("sign_type", rsa2 ? "RSA2" : "RSA");

		return keyValues;
	}

	/**
	 * 构造支付订单参数列表
	 */
	public static Map<String, String> buildOrderParamMap(String app_id, String total_amount,boolean rsa2) {
		Map<String, String> keyValues = new HashMap<String, String>();

		keyValues.put("app_id", app_id);

		keyValues.put("biz_content", "{\"timeout_express\":\"30m\",\"product_code\":\"QUICK_MSECURITY_PAY\",\"total_amount\":\"" + total_amount + "\",\"subject\":\"1\",\"body\":\"我是测试数据\",\"out_trade_no\":\"" + getOutTradeNo() +  "\"}");

		keyValues.put("charset", "utf-8");

		keyValues.put("method", "alipay.trade.app.pay");

		keyValues.put("sign_type", rsa2 ? "RSA2" : "RSA");

		keyValues.put("timestamp", "2024-10-21 12:00:00");

		keyValues.put("version", "1.0");

		return keyValues;
	}

	/**
	 * 构造支付订单参数信息
	 *
	 * @param map
	 * 支付订单参数
	 * @return
	 */
	public static String buildOrderParam(Map<String, String> map) {
		List<String> keys = new ArrayList<String>(map.keySet());

		StringBuilder sb = new StringBuilder();
		for (int i = 0; i < keys.size() - 1; i++) {
			String key = keys.get(i);
			String value = map.get(key);
			sb.append(buildKeyValue(key, value, true));
			sb.append("&");
		}

		String tailKey = keys.get(keys.size() - 1);
		String tailValue = map.get(tailKey);
		sb.append(buildKeyValue(tailKey, tailValue, true));

		return sb.toString();
	}

	/**
	 * 拼接键值对
	 *
	 * @param key
	 * @param value
	 * @param isEncode
	 * @return
	 */
	private static String buildKeyValue(String key, String value, boolean isEncode) {
		StringBuilder sb = new StringBuilder();
		sb.append(key);
		sb.append("=");
		if (isEncode) {
			try {
				sb.append(URLEncoder.encode(value, "UTF-8"));
			} catch (UnsupportedEncodingException e) {
				sb.append(value);
			}
		} else {
			sb.append(value);
		}
		return sb.toString();
	}

	/**
	 * 对支付参数信息进行签名
	 *
	 * @param map
	 *            待签名授权信息
	 *
	 * @return
	 */
	public static String getSign(Map<String, String> map, String rsaKey, boolean rsa2) {
		List<String> keys = new ArrayList<String>(map.keySet());
		// key排序
		Collections.sort(keys);

		StringBuilder authInfo = new StringBuilder();
		for (int i = 0; i < keys.size() - 1; i++) {
			String key = keys.get(i);
			String value = map.get(key);
			authInfo.append(buildKeyValue(key, value, false));
			authInfo.append("&");
		}

		String tailKey = keys.get(keys.size() - 1);
		String tailValue = map.get(tailKey);
		authInfo.append(buildKeyValue(tailKey, tailValue, false));

		String oriSign = SignUtils.sign(authInfo.toString(), rsaKey, rsa2);
		String encodedSign = "";

		try {
			encodedSign = URLEncoder.encode(oriSign, "UTF-8");
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		}
		return "sign=" + encodedSign;
	}

	/**
	 * 要求外部订单号必须唯一。
	 * @return
	 */
	private static String getOutTradeNo() {
		SimpleDateFormat format = new SimpleDateFormat("MMddHHmmss", Locale.getDefault());
		Date date = new Date();
		String key = format.format(date);

		Random r = new Random();
		key = key + r.nextInt();
		key = key.substring(0, 15);
		return key;
	}

}

第二个 SignUtils

public class SignUtils {

	private static final String ALGORITHM = "RSA";

	private static final String SIGN_ALGORITHMS = "SHA1WithRSA";

	private static final String SIGN_SHA256RSA_ALGORITHMS = "SHA256WithRSA";

	private static final String DEFAULT_CHARSET = "UTF-8";

	private static String getAlgorithms(boolean rsa2) {
		return rsa2 ? SIGN_SHA256RSA_ALGORITHMS : SIGN_ALGORITHMS;
	}

	public static String sign(String content, String privateKey, boolean rsa2) {
		try {
			PKCS8EncodedKeySpec priPKCS8 = new PKCS8EncodedKeySpec(
					Base64.decode(privateKey,Base64.DEFAULT));
			KeyFactory keyf = KeyFactory.getInstance(ALGORITHM);
			PrivateKey priKey = keyf.generatePrivate(priPKCS8);

			java.security.Signature signature = java.security.Signature
					.getInstance(getAlgorithms(rsa2));

			signature.initSign(priKey);
			signature.update(content.getBytes(DEFAULT_CHARSET));

			byte[] signed = signature.sign();

			return Base64.encodeToString(signed, Base64.DEFAULT);
		} catch (Exception e) {
			e.printStackTrace();
		}

		return null;
	}

}

第三个SnowflakeIdGenerator

public class SnowflakeIdGenerator {
    private static final long START_EPOCH = 1609459200000L; // 2021-01-01 00:00:00
    private static final long MACHINE_ID_BITS = 5L;
    private static final long SEQUENCE_BITS = 15L;
    private static final long MAX_MACHINE_ID = ~(-1L << MACHINE_ID_BITS);
    private static final long MAX_SEQUENCE = ~(-1L << SEQUENCE_BITS);
    private long machineId;
    private long sequence = 0L;
    private long lastTimestamp = -1L;

    public SnowflakeIdGenerator(long machineId) {
        if (machineId > MAX_MACHINE_ID || machineId < 0) {
            throw new IllegalArgumentException("Machine ID can't be greater than " + MAX_MACHINE_ID + " or less than 0");
        }
        this.machineId = machineId;
    }

    public synchronized String nextId() {
        long timestamp = System.currentTimeMillis();
        // 检查时间戳是否回退
        if (timestamp < lastTimestamp) {
            throw new RuntimeException("Clock moved backwards. Refusing to generate ID");
        }
        if (lastTimestamp == timestamp) {
            // 如果在同一毫秒内,增加序列号
            sequence = (sequence + 1) & MAX_SEQUENCE;
            if (sequence == 0) {
                // 序列号达到最大值,需要等待下一毫秒
                timestamp = waitNextMillis(lastTimestamp);
            }
        } else {
            // 不同毫秒,重置序列号
            sequence = 0L;
        }
        lastTimestamp = timestamp;
        long uniqueId = ((timestamp - START_EPOCH) << (MACHINE_ID_BITS + SEQUENCE_BITS)) | (machineId << SEQUENCE_BITS) | sequence;
        Log.d("uniqueId = ", String.valueOf(uniqueId));
        String idStr = String.valueOf(uniqueId).substring(0, 18);
        return idStr;
    }

    private long waitNextMillis(long lastTimestamp) {
        long timestamp;
        try {
            while ((timestamp = System.currentTimeMillis()) <= lastTimestamp) {
                Thread.sleep(1);
            }
        } catch (InterruptedException e) {
            throw new RuntimeException("Interrupted while waiting for next millisecond", e);
        }
        return timestamp;
    }

    private String toBase36(long id) {
        return Long.toString(id, 36);
    }
}

最后给出MainActivity.java

public class MainActivity extends AppCompatActivity {

    public static final String APPID = "9021000141649348";
    //私钥
    public static final String RSA2_PRIVATE = "MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQC4Stt4NtX1bazrqznm/NCLDHh54X+upPfWpZkRDwe/1BNAqR094NaD+2OfrEZZ5sWU5K/NMM2DSqnJ7qSIUpKira+0gh/0Iadtj+MyH17V5zBCsHHnPD1h+fuJfY8z6zUEEkPbObzWydephd1KP+6vGuHhOe9EIrSJWGD6VddfKEFPtFFFdNEUVHXi+cURPojRwUhz5ZZkrH5jJWL0n9COd3eYQ3hict/7gWWvLtRGcvsI/WkHMHnzWLO/X/LTSN2q+UIIb2zT1udN+o4Rl2GHUqITWR310WULNNFcUN9ilPa8F84XxjmTFn9B28Oj7we65cvFNTPllazaSlAVUXM7AgMBAAECggEBAJkzIV69tv9fPPBsVqX+ZB4zL7OiEVJNMPnuj1/u7rI4yghmjThzP+BXimmh/JmfjYBI2xvoBe+ukZacG4p3mg5B9F5KDeYUU+pwQp3YFi4bEbNemlFvcleQR9nz1qeCT4Ai7uW/CYxEHlN+RIcEvpYx8rQYRR6O+yVxoqx3z6H5m3CprcGbS3Qe5JBdD55WiWaMmuWS3pWFBpmtetNv6qGAvxgocXcLaAQ/6XDhOBeMFq68fFKdZqd4OYMWI2STzz9sR0oX8OO/0unFqGWXViclqPnJ/Z8nFTLFWliyJyGIMafl26b0Rx2HM3692HUu1J7ahurblE0iu/6tdPcNnwECgYEA4XJy9joXdV3d2RwEgzsKbJjtMfvzZudRLP8z4os9Ub2ydyZaP9rBjd7pY4+LmPFIzVILUcLz7R84CKyW/aG+Lv1B3o7fxESkWnfP/EyEJgx8zfEHLobJW/2X3F4qIkv4xbmcOPn1kk0Lkw8uRWSrfopTJsmVIwk46aIS9fvweZkCgYEA0USbzxMkKEo5v4P2PBG4Ul4qngOlx4vs5DrfvLReEZEvm/U4pQW3CgMk/QHj2POOjpNLNOSh7d4h3dgkuflFRRsN5K94rQP0RFCGW7w0aIWbL1WU0FcGliBcAw+vf5Ijzr6OgxZfSSKjnZpsue27CBxG+5GuDc//yvT1LF3In/MCgYEAtknzOLLKCwVl/0nPQEj56ctRZywgqCD7mxWS32fUogZvijYBnUYFYPBP6EfGCVl3k0T2kBrBXwbyKNlckSI6BAaVPx5pQmp6NghQrOE1rQpF08NDGlSz9eS76Nxe1zJ0qXOmJM+/x5byd+s7b7Kxk/TGvUMbiqPHV+nLyQf4bmkCgYEA0S6JqLZzgCqiCwR30JfN7dffNdBjmFIQXBtVpqWNGnZMZtL66koKK2H1SUroXOco6u/lT1vzWXif1cfG/ndjfK6MdrnIIPpA40Cy7WP15z0WYHxlotQ66zoxf4XgYd7NGE522iY03UBY2KOSZ1BxkqvhcHqwx3HROSkfIlgkwW0CgYEAxzGTFYu/jm1EfzBY75VyO1aaOqKTgOFUXvG1sh9P3zuOZqKZA2p5j27Stnd0auUaLb8/LhRp8OheJsKMso8JDXw8DaYvWqZLdT+sHthT57Wu3MfmKZZ9IGd9rxT3Q/JsuyyGlnV9ZRdUrPpwlwDNNI48uwRxuGTEqGHZmLTaGZU=";
    private RecyclerView rc_view;
    //创建一个List  原始数据源
    private List<RcBean> list = new ArrayList<>();
    //渲染用的列表
    private List<RcBean> newList = new ArrayList<>();
    //实例化Adapter
    RcAdapter adapter = new RcAdapter(newList);
    public static final int SDK_PAY_FLAG = 0x200;
    private static final String TAG = "====日志====";

    private final Handler mHandler = new Handler() {
        @SuppressLint("HandlerLeak")
        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
            switch (msg.what) {
                case SDK_PAY_FLAG:

                    Map<String, String> resultMap = (Map<String, String>) (Map<String, String>)msg.obj;

                    if (resultMap.get("resultStatus").equals("9000")) {
                        Toast.makeText(MainActivity.this, "支付成功", Toast.LENGTH_SHORT).show();
                    }

                    if (resultMap.get("resultStatus").equals("6001")) {
                        Toast.makeText(MainActivity.this, resultMap.get("memo").toString(), Toast.LENGTH_SHORT).show();
                    }

                    break;
            }
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //进入沙盒环境
        EnvUtils.setEnv(EnvUtils.EnvEnum.SANDBOX);
        setContentView(R.layout.activity_main);

        //初始化控件
        initView();
        //初始化加数据
        setData();
        //点击方法
        setOnClickListener();

    }

    private void setData() {

        /*这里用两个List另有他用,有点经验的应该知道,新手朋友造写吧*/
        list.clear();
        newList.clear();
        RcBean rcBean1 = new RcBean("斐济杯", new BigDecimal(1000.00));
        RcBean rcBean2 = new RcBean("刀魔", new BigDecimal(999.00));
        list.add(rcBean1);
        list.add(rcBean2);

        newList.addAll(list);



    }

    private void setOnClickListener() {

        adapter.setOnClickListener(position -> {

            Log.d(TAG, "newList.get(position).getMoney(): " + newList.get(position).getMoney());

            // TODO: 2024/10/23 这里写支付代码模块
            Map<String, String> params = OrderInfoUtil2_0.buildOrderParamMap(APPID, String.valueOf(newList.get(position).getMoney()), true);
            String orderParam = OrderInfoUtil2_0.buildOrderParam(params);
            String privateKey = RSA2_PRIVATE;
            String sign = OrderInfoUtil2_0.getSign(params, privateKey, true);

            final String orderInfo = orderParam + "&" + sign;

            Log.d(TAG, "orderInfo: " + orderInfo);

            Runnable payRunnable = new Runnable() {
                @Override
                public void run() {
                    AuthTask alipay = new AuthTask(MainActivity.this);
                    Map<String, String> result = alipay.authV2(orderInfo, true);
                    Message msg = new Message();
                    msg.what = SDK_PAY_FLAG;
                    msg.obj = result;
                    mHandler.sendMessage(msg);

                }
            };

            Thread payThread = new Thread(payRunnable);
            payThread.start();

        });


    }

    private void initView() {
        rc_view = findViewById(R.id.rc_view);
        rc_view.setLayoutManager(new LinearLayoutManager(this));
        rc_view.setAdapter(adapter);



    }
}
三.测试效果

至此整个项目已经全部完成了,启动测试,看效果

 

当前项目已经放入gitee有意者可自取 地址:无敌最俊朗/csdn-支付宝沙箱

SpringBoot + Vue.js 结合支付宝 sandbox 模拟支付通常涉及以下几个步骤: 1. **环境配置**: - 首先,在Vue项目中安装axios等HTTP客户端库用于发送请求。 - 确保已获取到支付宝的商户账号、私钥以及对应的沙箱测试环境信息。 2. **引入支付宝 SDK**: - 下载官方提供的支付宝SDK(AlipayJSAPIBridge)并将其引入到Vue项目中。 - 通常是通过npm或cdn的方式引入。 3. **创建支付请求**: - 创建一个函数,使用`AlipayJSAPIBridge.invoke`发起支付请求,传入模拟数据,如商品信息、订单金额等。 ```javascript async createPayRequest(data) { const aliConfig = { // 填写支付宝沙箱环境参数 'app_id': 'your_sandbox_app_id', 'method': 'alipay.trade.page.pay', 'format': 'json', 'charset': 'utf-8', 'sign_type': 'RSA2', 'timestamp': Math.round(new Date().getTime() / 1000), 'version': '1.0', 'notify_url': '', // 实际生产环境下的通知接收地址,这里是测试环境无需填写 'return_url': '', // 支付完成后跳转回的页面URL,测试环境无需填写 }; aliConfig['params'] = { ...data, 'sign': sign(aliConfig, your_private_key), // 使用私钥对参数进行签名 }; const response = await axios.post('/alipay/pay', aliConfig); // 处理返回结果,比如跳转到支付宝支付页面 } ``` 4. **处理回调**: - 当用户在支付宝完成支付后,会通过postback或异步通知服务回到你的应用。你需要实现相应的处理逻辑,校验通知中的数据是否合法。 5. **注意点**: - 在开发过程中,记得始终使用沙箱环境,以便在安全环境下测试。 - 对于敏感信息(如私钥),需要妥善保管,并在实际部署时替换为正式环境的值。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

FireDuration

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值