需要一部安卓机,开发一款个人收款。
Live-demo
网络上有许多的第三方提供了 app 或者 网站 让我们上传自己的收款码,它为我们提供 hook ,部分需要收费比如 pay.js ,当然肯定不止一个收费的。
免费的我也害怕,因为我不知道它怎么实现,所以我也有点担心会不会收费收费突然就被 cut 掉了。
所以最好的方法就是 自己做一套。 自己知根知底,就不会出错了。
这篇文章合适人选: 想要做一套个人收款的开发。
技术包括但不限于 Android、web、node
作为一名web开发人员,Android 是最难的问题。本来可以一天搞完的事情,硬生生被 Android 拖了 n 久。
个人就踩过坑,什么CA证书,http请求不安全,service,权限等等。都是让我懵逼的,现在踩过坑了。写一篇文章出来,让大家都可以做一个自己的收款app。
先说下思路,我这边的思路是只对一个 order表 进行操作。
用户在一个页面中,进行付款,会产生一个支付订单,时效只有5分钟,这个我放在redis里,如果超过5分钟,自动生成新的支付订单,并且删除老的订单,价格是我往下浮动2毛钱,就是说 5分钟 内 2元钱,只能同时有20个人并发,这个对个人用户来说应该足以,但是如果对于商用,是远远不够的。而且我这里还有一个字段 key ,根据这个key可以让同一个用户拉取同样价格支付订单,但是是不同的价格。以此区分用户付款后对应的是哪一个真正的订单。用户支付完成后,根据真实价格,反推出订单,即可。
订单表 主要有
| field | name | describe |
|---|---|---|
| id | 订单id | 订单唯一标志 |
| user | 持单用户 | 标记用户信息 |
| page | 购买页面 | 目前我选择是页面,当然也可以是任何东西 |
| isPayed | 支付状态 | 判断是否支付 |
| price | 真实支付价格 | 记录真实价格 |
支付订单 用户
{
leon: {
20: [{price:19.8, key: 'page1', expired: 1574069463854}, {price:19.3, key: 'page2', expired: 1574069463854}] // leon同时在20元的区间里,拥有两个订单,如果订单过期或者支付成功,需要把价格重置到价格管理上
}
}
价格管理
{
20: [20, 19.9, 19.8, ..., 19.01] // 20条数据,当前价格允许的浮动下,也可以进行30条,如果能接受的话。 :)
30: [30, 29.9, 29.8, ..., 29.01] // ....
}
主要用微信收款来说吧,因为当你看了下面的代码,你会发现支付宝也是一样的。
首先创建一个 android 的项目
找到上面的文件 AndroidManifest.xml
增加监控权限和网络请求权限 并且增加 service 的 NotificationListener ,这里主要是因为要重写这里的监听。
这里直接贴代码了。如果不想敲代码的话直接拷贝
AndroidManifest
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.z.monitor">
<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY"/>
<uses-permission android:name="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"/>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service android:name=".NotificationListener"
android:label="通知监听程序"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
<intent-filter>
<action android:name="android.service.NotificationListenerService"/>
</intent-filter>
</service>
</application>
</manifest>
修改 MainActivity 的代码,允许app获取监听的权限。
MainActivity
package com.z.monitor;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.os.Bundle;
import android.provider.Settings;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
openNotificationListenSettings();
}
/**
* 打开监听页面设置
*/
public void openNotificationListenSettings() {
try {
Intent intent;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP_MR1) {
intent = new Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS);
} else {
intent = new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS");
}
startActivity(intent);
} catch (Exception e) {
e.printStackTrace();
}
}
}
NotificationListener 上的方法
package com.z.monitor;
import android.annotation.TargetApi;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.text.TextUtils;
import android.util.Log;
import android.widget.RemoteViews;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Field;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
public class NotificationListener extends NotificationListenerService {
private static final String TAG = "NotificationListener";
private static String PATH = "https://example.com"; // 回调的钩子页面
private static URL url;
static{
try {
url = new URL(PATH);
} catch (MalformedURLException e) {
e.printStackTrace();
}
}
@Override
public void onNotificationPosted(StatusBarNotification sbn) {
// 如果不是支付宝或者微信就直接忽略
if (!"com.tencent.mm".equals(sbn.getPackageName()) &&
!"com.eg.android.AlipayGphone".equals(sbn.getPackageName())) {
return;
}
Notification notification = sbn.getNotification();
if (notification == null) {
return;
}
String realPrice = "";
PendingIntent pendingIntent = null;
// 当 API > 18 时,使用 extras 获取通知的详细信息
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
Bundle extras = notification.extras;
if (extras != null) {
// 获取通知标题
String title = extras.getString(Notification.EXTRA_TITLE, "");
Log.d(TAG,title);
// 获取通知内容
String content = extras.getString(Notification.EXTRA_TEXT, "");
Log.d(TAG, content);
if (!TextUtils.isEmpty(content)) {
if (content.contains("成功收款")) {
try {
String[] _tmp = content.split("元")[0].split("成功收款");
realPrice = _tmp[1];
} catch (Exception e) {
realPrice = "";
}
}
if (content.contains("微信支付收款")) {
try {
pendingIntent = notification.contentIntent;
String[] _tmp = content.split("微信支付收款")[1].split("元");
realPrice = _tmp[0];
} catch (Exception e) {
realPrice = "";
}
}
}
}
} else {
// 当 API = 18 时,利用反射获取内容字段
List<String> textList = getText(notification);
if (textList != null && textList.size() > 0) {
for (String text : textList) {
if (!TextUtils.isEmpty(text)){
if (text.contains("成功收款")) {
try {
String[] _tmp = text.split("元")[0].split("成功收款");
realPrice = _tmp[1];
} catch (Exception e) {
realPrice = "";
}
}
if (text.contains("微信支付收款")) {
pendingIntent = notification.contentIntent;
try {
String[] _tmp = text.split("微信支付收款")[1].split("元");
realPrice = _tmp[0];
} catch (Exception e) {
realPrice = "";
}
}
break;
}
}
}
}
// 发送 pendingIntent 以此打开微信
if (realPrice != "") {
Map<String, String> params = new HashMap<String, String>();
params.put("realPrice", realPrice);
String result = sendPostMessage(params,"utf-8");
System.out.println("result->"+result);
}
// 发送 pendingIntent 以此打开微信
try {
if (pendingIntent != null) {
pendingIntent.send();
moveBack();
}
} catch (PendingIntent.CanceledException e) {
e.printStackTrace();
}
}
public List<String> getText(Notification notification) {
if (null == notification) {
return null;
}
RemoteViews views = notification.bigContentView;
if (views == null) {
views = notification.contentView;
}
if (views == null) {
return null;
}
// Use reflection to examine the m_actions member of the given RemoteViews object.
// It's not pretty, but it works.
List<String> text = new ArrayList<>();
try {
Field field = views.getClass().getDeclaredField("mActions");
field.setAccessible(true);
@SuppressWarnings("unchecked")
ArrayList<Parcelable> actions = (ArrayList<Parcelable>) field.get(views);
// Find the setText() and setTime() reflection actions
for (Parcelable p : actions) {
Parcel parcel = Parcel.obtain();
p.writeToParcel(parcel, 0);
parcel.setDataPosition(0);
// The tag tells which type of action it is (2 is ReflectionAction, from the source)
int tag = parcel.readInt();
if (tag != 2) continue;
// View ID
parcel.readInt();
String methodName = parcel.readString();
if (null == methodName) {
continue;
} else if (methodName.equals("setText")) {
// Parameter type (10 = Character Sequence)
parcel.readInt();
// Store the actual string
String t = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel).toString().trim();
text.add(t);
}
parcel.recycle();
}
} catch (Exception e) {
e.printStackTrace();
}
return text;
}
public static String sendPostMessage(Map<String, String> params, String encode){
StringBuffer buffer = new StringBuffer();
try {//把请求的主体写入正文!!
if(params != null&&!params.isEmpty()){
for(Map.Entry<String, String> entry : params.entrySet()){
buffer.append(entry.getKey()).append("=").
append(URLEncoder.encode(entry.getValue(),encode)).
append("&");
}
}
// System.out.println(buffer.toString());
//删除最后一个字符&,多了一个;主体设置完毕
buffer.deleteCharAt(buffer.length()-1);
byte[] mydata = buffer.toString().getBytes();
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setConnectTimeout(3000);
connection.setDoInput(true);//表示从服务器获取数据
connection.setDoOutput(true);//表示向服务器写数据
connection.setRequestMethod("POST");
//是否使用缓存
connection.setUseCaches(false);
//表示设置请求体的类型是文本类型
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
connection.setRequestProperty("Content-Length", String.valueOf(mydata.length));
connection.connect(); //连接,不写也可以。。??有待了解
//获得输出流,向服务器输出数据
OutputStream outputStream = connection.getOutputStream();
outputStream.write(mydata,0,mydata.length);
//获得服务器响应的结果和状态码
int responseCode = connection.getResponseCode();
if(responseCode == HttpURLConnection.HTTP_OK){
return changeInputeStream(connection.getInputStream(),encode);
}
} catch (UnsupportedEncodingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return "";
}
/**
* 将一个输入流转换成字符串
* @param inputStream
* @param encode
* @return
*/
private static String changeInputeStream(InputStream inputStream,String encode) {
//通常叫做内存流,写在内存中的
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
byte[] data = new byte[1024];
int len = 0;
String result = "";
if(inputStream != null){
try {
while((len = inputStream.read(data))!=-1){
data.toString();
outputStream.write(data, 0, len);
}
//result是在服务器端设置的doPost函数中的
result = new String(outputStream.toByteArray(),encode);
outputStream.flush();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return result;
}
/**
* 触发 Home 键,回到主桌面
* @return true
*/
public boolean moveBack() {
Intent backHome = new Intent(Intent.ACTION_MAIN);
backHome.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
backHome.addCategory(Intent.CATEGORY_HOME);
startActivity(backHome);
return true;
}
}
这个方法只有 minSdkVersion 在 18 以上才支持。看提示的,不过我把它弄成21了,一样好用。 在 src/build.gradle 上修改
build.gradle
apply plugin: 'com.android.application'
android {
compileSdkVersion 29
buildToolsVersion "29.0.2"
defaultConfig {
applicationId "com.z.monitor"
minSdkVersion 21
targetSdkVersion 29
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}
这里好多代码都是我东拼西凑的,主要做的就是一件事情,如果有 微信/支付宝 的消息过来,我就对其截取,截取到价格之后,发送到支付成功的回调里。
这里尤其注意的一点就是,必须要用 https 。阿里云配置https还是很简单的 :) 反正我配好了,怎么说都可以。
插入手机,打包。
对于web或者其他非Android开发人员来说,已经足够了。因为我们只要做到,能监听到钩子,其他的不过是业务逻辑上的问题而已。
其他的我就不一步步写了。主要是因为因为写的不严谨 :) ,改了几版。 我就直接传到 github 上去了。
用 3毛钱 做一个 测试demo , 有2毛是作为下浮的,大伙可以随便玩一下。
喜欢就随缘star一下项目 github.com/no-ane/pers…