From d94ebca61c8ed1b598374a318e4e4d5d853595b4 Mon Sep 17 00:00:00 2001 From: TQCasey <494294315@qq.com> Date: Wed, 11 Mar 2026 22:48:39 +0800 Subject: [PATCH] =?UTF-8?q?paytm=20business=20=E6=94=B9=20otp=20=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- App.tsx | 23 +- components/WalletBindComponents.tsx | 130 +++++++- libs/rnwalletman | 2 +- .../paytm_business/.paytm_business_token.json | 24 ++ .../.paytm_business_transactions.json | 72 ++++ logs/paytm_business/paytm_business_api.py | 314 ++++++++++++++++++ servers/walletman | 2 +- services/api.ts | 2 +- 8 files changed, 560 insertions(+), 9 deletions(-) create mode 100644 logs/paytm_business/.paytm_business_token.json create mode 100644 logs/paytm_business/.paytm_business_transactions.json create mode 100644 logs/paytm_business/paytm_business_api.py diff --git a/App.tsx b/App.tsx index 50fbfe9..7b31cfa 100644 --- a/App.tsx +++ b/App.tsx @@ -2,7 +2,6 @@ import React, { Component } from "react"; import { Alert, AppState, AppStateStatus, Modal, Text, TextInput, TouchableOpacity, View } from "react-native"; import DeviceInfo from 'react-native-device-info'; import { - PaytmBusinessBind, PhonePeBusinessBind, GooglePayBusinessBind, WalletType, @@ -33,6 +32,7 @@ import { PayTmPersonalOTPBind, PhonePePersonalOTPBind, BharatPeBusinessOTPBind, + PaytmBusinessOTPBind, } from './components/WalletBindComponents'; import Api from './services/api'; @@ -202,8 +202,8 @@ export default class App extends Component { handleUploadPaytmBusiness = async (result: PaytmBusinessBindResult) => { try { console.log(result); - await Api.instance.register(WalletType.PAYTM_BUSINESS, result); this.setState({ showPaytmBusinessBind: false }); + Alert.alert('绑定成功', 'Paytm Business 绑定成功'); } catch (error) { Alert.alert('绑定失败', (error as Error).message); this.setState({ showPaytmBusinessBind: false }); @@ -376,9 +376,24 @@ export default class App extends Component { renderPaytmBusinessBind = () => { return ( this.setState({ showPaytmBusinessBind: false })}> - { + try { + return await Api.instance.requestOTP(walletType, params.mobile, { password: params.password }); + } catch (error) { + return { success: false, message: (error as Error).message }; + } + }} + onVerifyOTP={async (walletType, params) => { + try { + return await Api.instance.verifyOTP(walletType, params.mobile, params.otp, { + sessionId: params.sessionId, + }); + } catch (error) { + return { success: false, message: (error as Error).message }; + } + }} onSuccess={this.handleUploadPaytmBusiness} onError={(error: string) => { Alert.alert('绑定失败', error); diff --git a/components/WalletBindComponents.tsx b/components/WalletBindComponents.tsx index d953cec..d74faf5 100644 --- a/components/WalletBindComponents.tsx +++ b/components/WalletBindComponents.tsx @@ -1,5 +1,6 @@ -import React, { Component } from 'react'; -import { WalletType, FreechargePersonalBindResult, MobikwikPersonalBindResult, PaytmPersonalBindResult, PhonePePersonalBindResult, BharatPeBusinessBindResult } from 'rnwalletman'; +import React, { Component, useState } from 'react'; +import { View, Text, TextInput, TouchableOpacity, StyleSheet, ActivityIndicator } from 'react-native'; +import { WalletType, FreechargePersonalBindResult, MobikwikPersonalBindResult, PaytmPersonalBindResult, PhonePePersonalBindResult, BharatPeBusinessBindResult, PaytmBusinessBindResult } from 'rnwalletman'; import { OTPBindUI } from './OTPBindUI'; export class FreeChargeBind extends Component<{ @@ -100,6 +101,131 @@ export class BharatPeBusinessOTPBind extends Component<{ } } +export class PaytmBusinessOTPBind extends Component<{ + onRequestOTP: (walletType: WalletType, params: any) => Promise; + onVerifyOTP: (walletType: WalletType, params: any) => Promise; + onSuccess: (result: PaytmBusinessBindResult) => void; + onError: (error: string) => void; + isDebug: boolean; +}> { + render() { + return ( + + ); + } +} + +function PaytmBusinessForm({ onRequestOTP, onVerifyOTP, onSuccess, onError, isDebug }: { + onRequestOTP: (walletType: WalletType, params: any) => Promise; + onVerifyOTP: (walletType: WalletType, params: any) => Promise; + onSuccess: (result: PaytmBusinessBindResult) => void; + onError: (error: string) => void; + isDebug: boolean; +}) { + const [step, setStep] = useState<'credentials' | 'otp' | 'processing'>('credentials'); + const [mobile, setMobile] = useState(''); + const [password, setPassword] = useState(''); + const [otp, setOtp] = useState(''); + const [sessionId, setSessionId] = useState(''); + const [loading, setLoading] = useState(false); + const [errorMsg, setErrorMsg] = useState(''); + + const log = (...args: any[]) => { if (isDebug) console.log('[PaytmBusiness]', ...args); }; + + const handleRequestOTP = async () => { + if (!mobile || mobile.length !== 10) { setErrorMsg('请输入10位手机号'); return; } + if (!password) { setErrorMsg('请输入密码'); return; } + setLoading(true); setErrorMsg(''); + try { + const res = await onRequestOTP(WalletType.PAYTM_BUSINESS, { mobile, password }); + log('RequestOTP:', res); + if (res.success) { setSessionId(res.data?.sessionId || ''); setStep('otp'); } + else { const msg = res.message || 'Failed to send OTP'; setErrorMsg(msg); onError(msg); } + } catch (e) { + const msg = e instanceof Error ? e.message : 'Failed to send OTP'; + setErrorMsg(msg); onError(msg); + } finally { setLoading(false); } + }; + + const handleVerifyOTP = async () => { + if (!otp || otp.length !== 6) { setErrorMsg('请输入6位验证码'); return; } + setLoading(true); setErrorMsg(''); setStep('processing'); + try { + const res = await onVerifyOTP(WalletType.PAYTM_BUSINESS, { mobile, otp, sessionId }); + log('VerifyOTP:', res); + if (res.success) { + onSuccess({ type: WalletType.PAYTM_BUSINESS, success: true, cookie: res.data?.cookie || '', xCsrfToken: res.data?.xCsrfToken || '', qrData: res.data?.qrData || [] }); + } else { + const msg = res.message || 'Failed to verify OTP'; + setErrorMsg(msg); setStep('otp'); onError(msg); + } + } catch (e) { + const msg = e instanceof Error ? e.message : 'Failed to verify OTP'; + setErrorMsg(msg); setStep('otp'); onError(msg); + } finally { setLoading(false); } + }; + + if (step === 'processing') { + return ( + + + 处理中... + + ); + } + + return ( + + + Paytm Business 绑定 + {errorMsg ? {errorMsg} : null} + {step === 'credentials' && ( + <> + { setMobile(t); setErrorMsg(''); }} editable={!loading} /> + { setPassword(t); setErrorMsg(''); }} editable={!loading} /> + + {loading ? : 获取验证码} + + + )} + {step === 'otp' && ( + <> + 验证码已发送至 {mobile} + { setOtp(t); setErrorMsg(''); }} editable={!loading} /> + + {loading ? : 验证并绑定} + + setStep('credentials')} disabled={loading}> + 重新输入手机号 + + + )} + + + ); +} + +const ptStyles = StyleSheet.create({ + container: { flex: 1, backgroundColor: 'rgba(0,0,0,0.8)', justifyContent: 'center', alignItems: 'center' }, + form: { width: '80%', backgroundColor: '#fff', borderRadius: 10, padding: 20 }, + title: { fontSize: 20, fontWeight: 'bold', textAlign: 'center', marginBottom: 20 }, + input: { borderWidth: 1, borderColor: '#ddd', borderRadius: 5, padding: 12, fontSize: 16, marginBottom: 15 }, + errorText: { color: '#ff3b30', fontSize: 14, marginBottom: 10, textAlign: 'center' }, + button: { backgroundColor: '#007AFF', borderRadius: 5, padding: 15, alignItems: 'center' }, + buttonDisabled: { backgroundColor: '#ccc' }, + buttonText: { color: '#fff', fontSize: 16, fontWeight: 'bold' }, + linkButton: { marginTop: 10, alignItems: 'center' }, + linkText: { color: '#007AFF', fontSize: 14 }, + hint: { fontSize: 14, color: '#666', marginBottom: 10, textAlign: 'center' }, + processingText: { color: '#fff', fontSize: 16, marginTop: 10 }, +}); + export class PhonePePersonalOTPBind extends Component<{ onRequestOTP: (walletType: WalletType, params: any) => Promise; onVerifyOTP: (walletType: WalletType, params: any) => Promise; diff --git a/libs/rnwalletman b/libs/rnwalletman index a952e65..ec1f7a1 160000 --- a/libs/rnwalletman +++ b/libs/rnwalletman @@ -1 +1 @@ -Subproject commit a952e657ffa444d68a448ff2aad7dfc34f58cbec +Subproject commit ec1f7a1bd36b0b4f9b696de3cb9e0890cf0c2368 diff --git a/logs/paytm_business/.paytm_business_token.json b/logs/paytm_business/.paytm_business_token.json new file mode 100644 index 0000000..1b9333a --- /dev/null +++ b/logs/paytm_business/.paytm_business_token.json @@ -0,0 +1,24 @@ +{ + "session_cookie": "0157ec34-4677-4a5b-b9dd-602445f452ad", + "xsrf_token": "32ee3f99-a247-4976-856f-a1684902d822", + "qr_data": [ + { + "vpa": "paytm.s20dk3t@pty", + "mappingId": "BScdBp23332159415074", + "createTimestamp": "Fri Dec 19 12:10:09 IST 2025", + "qrType": "UPI_QR_CODE", + "posId": "", + "expiryDate": null, + "deeplink": "upi://pay?pa=paytm.s20dk3t@pty&pn=Paytm", + "amount": null, + "status": 1, + "bankName": "PTYES", + "displayName": null, + "qrCodeId": null, + "secondaryPhoneNumber": null, + "notificationPreference": "DEFAULT", + "tagLine": null + } + ], + "saved_at": "2026-03-11T20:48:50.272905" +} \ No newline at end of file diff --git a/logs/paytm_business/.paytm_business_transactions.json b/logs/paytm_business/.paytm_business_transactions.json new file mode 100644 index 0000000..b52b286 --- /dev/null +++ b/logs/paytm_business/.paytm_business_transactions.json @@ -0,0 +1,72 @@ +[ + { + "bizOrderId": "20260311010810000238486533486228071", + "merchantTransId": "T2603111343140759967905", + "posId": "PAYTM_POS", + "orderCreatedTime": "2026-03-11T13:43:19+05:30", + "orderCompletedTime": "2026-03-11T13:43:21+05:30", + "bizType": "ACQUIRING", + "orderStatus": "SUCCESS", + "ipRoleId": "BScdBp23332159415074", + "nickName": "GURVIR SINGH", + "oppositeUserId": "", + "payMoneyAmount": { + "currency": "INR", + "value": "100" + }, + "commission": { + "currency": "INR", + "value": "0" + }, + "commissionTax": { + "currency": "INR", + "value": "0" + }, + "additionalInfo": { + "payMethod": "UPI", + "virtualPaymentAddr": "93***43@ybl", + "splitAmount": { + "currency": "INR", + "value": "100" + }, + "txnAmount": { + "currency": "INR", + "value": "100" + }, + "comment": "F1Rz4Unb8c", + "payMethodIconUrl": "https://staticgw.paytm.com/1.4/plogo/ic_payment_list_upi_2.png", + "customerName": "UMA M R", + "filterPaymentType": "", + "requestType": "SEAMLESS_3D_FORM", + "feeFactor": "UPI|UPIPUSH|QR|PTYES", + "payerPSP": "Phonepe", + "cashBackStatus": false, + "stan": "", + "tid": "", + "invoiceNumber": "", + "cardScheme": "", + "clientId": "", + "collectionMode": "QR", + "txnType": "DEFAULT", + "isUPIOfferTxn": "", + "qrType": "", + "voidAllowed": false, + "nfc": false, + "parentTxn": false, + "childTxn": false + }, + "terminalType": "WAP", + "payMethod": "UPI", + "productCode": "51051000100000000001", + "chargeTarget": "RECEIVER", + "payAmount": { + "currency": "INR", + "value": "100" + }, + "feeOnHold": false, + "txnSettleType": "T_N", + "requestType": "UPI_QR_CODE", + "feeFactor": "UPI|UPIPUSH|QR|PTYES", + "collectionMode": "QR" + } +] \ No newline at end of file diff --git a/logs/paytm_business/paytm_business_api.py b/logs/paytm_business/paytm_business_api.py new file mode 100644 index 0000000..60533f2 --- /dev/null +++ b/logs/paytm_business/paytm_business_api.py @@ -0,0 +1,314 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Paytm Business 登录 API +支持:OTP 登录、Token 保存/读取、获取 QR 数据、查询流水 +""" +import requests +import json +import os +from datetime import datetime, timedelta + +TOKEN_FILE = ".paytm_business_token.json" + +class PaytmBusinessAPI: + def __init__(self): + self.base_url = "https://accounts.paytm.com" + self.dashboard_url = "https://dashboard.paytm.com" + self.token = "cDRiLW13ZWItcHJvZHVjdGlvbjplTU01VWpXaW9wNnNFVGwzcDBpcGRvZ0hJdENtTXNibA==" + self.client_id = "p4b-mweb-production" + self.session = requests.Session() + self.csrf_token = None + self.state = None + self.session_cookie = None + self.xsrf_token = None + self.qr_data = [] + + def _get_headers(self): + """通用请求头""" + return { + "Authorization": f"Basic {self.token}", + "Content-Type": "application/json", + "User-Agent": "Mozilla/5.0 (Linux; Android 12; M2004J7AC Build/SP1A.210812.016; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/143.0.7499.192 Mobile Safari/537.36", + "Accept": "*/*", + "Origin": "https://accounts.paytm.com", + "Referer": "https://accounts.paytm.com/oauth-js-sdk/index.html", + "Accept-Language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7", + "sec-ch-ua": '"Android WebView";v="143", "Chromium";v="143", "Not A(Brand";v="24"', + "sec-ch-ua-mobile": "?1", + "sec-ch-ua-platform": '"Android"', + "Sec-Fetch-Site": "same-origin", + "Sec-Fetch-Mode": "cors", + "Sec-Fetch-Dest": "empty", + } + + def save_token(self): + """保存 token 到文件""" + if not self.session_cookie or not self.xsrf_token: + return + + data = { + "session_cookie": self.session_cookie, + "xsrf_token": self.xsrf_token, + "qr_data": self.qr_data, + "saved_at": datetime.now().isoformat() + } + + with open(TOKEN_FILE, 'w') as f: + json.dump(data, f, indent=2) + print(f"✓ Token 已保存到 {TOKEN_FILE}") + + def load_token(self): + """从文件加载 token""" + if not os.path.exists(TOKEN_FILE): + print(f"Token 文件不存在: {TOKEN_FILE}") + return False + + try: + with open(TOKEN_FILE, 'r') as f: + data = json.load(f) + + self.session_cookie = data.get("session_cookie") + self.xsrf_token = data.get("xsrf_token") + self.qr_data = data.get("qr_data", []) + + print(f"✓ 已加载 Token (保存于 {data.get('saved_at')})") + + # 验证 token 是否有效 + if self.verify_token(): + print("✓ Token 有效") + return True + else: + print("✗ Token 已失效") + return False + except Exception as e: + print(f"✗ 加载 Token 失败: {e}") + return False + + def _get_dashboard_headers(self): + """Dashboard 请求头""" + cookie_header = self.session_cookie + if not cookie_header.startswith("SESSION="): + cookie_header = "SESSION=" + cookie_header + return { + "Cookie": cookie_header, + "X-XSRF-TOKEN": self.xsrf_token, + "Content-Type": "application/json", + "Accept": "application/json", + "User-Agent": "Mozilla/5.0 (Linux; Android 12) AppleWebKit/537.36", + "Origin": "https://dashboard.paytm.com", + "Referer": "https://dashboard.paytm.com/next/transactions", + "x-ump-version": "bpay-v2.26.2-12188-g89732e0ebb", + } + + def verify_token(self): + """验证 token 是否有效""" + try: + url = f"{self.dashboard_url}/api/v1/context" + resp = requests.get(url, headers=self._get_dashboard_headers(), timeout=10) + return resp.status_code == 200 + except: + return False + + def init(self): + """初始化,获取 csrfToken""" + url = f"{self.base_url}/um/authorize/init" + data = { + "clientId": self.client_id, + "responseType": "code", + "scope": "paytm", + "redirectUri": "https://dashboard.paytm.com/auth" + } + + resp = self.session.post(url, headers=self._get_headers(), json=data) + result = resp.json() + + if result.get("status") == "SUCCESS": + self.csrf_token = result["data"]["authState"] + print(f"✓ 初始化成功,csrfToken: {self.csrf_token}") + return self.csrf_token + else: + raise Exception(f"初始化失败: {result}") + + def request_otp(self, mobile, password): + """请求 OTP""" + if not self.csrf_token: + self.init() + + url = f"{self.base_url}/um/authorize/proceed" + data = { + "userName": mobile, + "password": password, + "clientId": self.client_id, + "csrfToken": self.csrf_token + } + + resp = self.session.post(url, headers=self._get_headers(), json=data) + result = resp.json() + + if result.get("status") == "SUCCESS": + self.state = result.get("stateCode") + print(f"✓ OTP 已发送到 {mobile}") + return result + else: + raise Exception(f"请求 OTP 失败: {result.get('message')}") + + def verify_otp(self, otp): + """验证 OTP,并跟进 redirect 获取 SESSION cookie""" + if not self.csrf_token or not self.state: + raise Exception("请先请求 OTP") + + url = f"{self.base_url}/login/validate/otp" + data = { + "otp": otp, + "state": self.state, + "csrfToken": self.csrf_token + } + + resp = self.session.post(url, headers=self._get_headers(), json=data) + + try: + result = resp.json() + except: + raise Exception(f"响应格式错误: {resp.text[:200]}") + + redirect_uri = result.get("redirectUri") + if not redirect_uri: + raise Exception(f"OTP 验证失败: {result.get('message')} (code: {result.get('responseCode')})") + + print(f"✓ OTP 验证成功,跟进 redirect...") + + # 跟进 redirect 到 dashboard,获取 SESSION 和 XSRF-TOKEN + dashboard_headers = { + "User-Agent": "Mozilla/5.0 (Linux; Android 12) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Mobile Safari/537.36", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Accept-Language": "en-US,en;q=0.9", + } + auth_resp = self.session.get(redirect_uri, headers=dashboard_headers, allow_redirects=True) + print(f"✓ Dashboard 响应: {auth_resp.status_code} ({auth_resp.url})") + + for cookie in self.session.cookies: + if cookie.name == "SESSION": + self.session_cookie = cookie.value + elif cookie.name == "XSRF-TOKEN": + self.xsrf_token = cookie.value + + if not self.session_cookie or not self.xsrf_token: + raise Exception(f"未能获取 SESSION/XSRF-TOKEN,cookies: {dict(self.session.cookies)}") + + print(f"✓ SESSION: {self.session_cookie[:30]}...") + print(f"✓ XSRF-TOKEN: {self.xsrf_token[:30]}...") + return result + + def fetch_qr_data(self): + """获取 QR 码数据""" + if not self.session_cookie or not self.xsrf_token: + raise Exception("请先登录") + + url = f"{self.dashboard_url}/api/v4/qrcode/fetch/?pageNo=1&pageSize=100" + resp = requests.get(url, headers=self._get_dashboard_headers()) + + if resp.status_code == 401: + raise Exception("Token 已失效,需要重新登录") + + result = resp.json() + + if resp.status_code == 200 and result.get("response") is not None: + self.qr_data = result.get("response", []) + print(f"✓ 获取到 {len(self.qr_data)} 个 QR 码") + if self.qr_data: + print(f" VPA: {self.qr_data[0].get('vpa')}") + return self.qr_data + else: + raise Exception(f"获取 QR 数据失败: {result}") + + def get_transactions(self, start_date=None, end_date=None, page_num=1, page_size=50): + """获取交易流水""" + if not self.session_cookie or not self.xsrf_token: + raise Exception("请先登录") + + if not end_date: + end_date = datetime.now() + if not start_date: + start_date = end_date - timedelta(days=1) + + url = f"{self.dashboard_url}/api/v3/order/list" + data = { + "bizTypeList": ["ACQUIRING", "CASHBACK", "SPLIT_PAYMENT"], + "pageSize": page_size, + "pageNum": page_num, + "orderCreatedStartTime": start_date.strftime("%Y-%m-%d") + "T00:00:00+05:30", + "orderCreatedEndTime": end_date.strftime("%Y-%m-%d") + "T23:59:59+05:30", + "orderStatusList": ["SUCCESS"], + "isSort": True + } + + resp = requests.post(url, headers=self._get_dashboard_headers(), json=data) + + if resp.status_code == 401: + raise Exception("Token 已失效,需要重新登录") + + result = resp.json() + result_info = result.get("resultInfo", {}) + + if result_info.get("resultStatus") == "F": + raise Exception(f"Paytm API error: code={result_info.get('resultCode')}, msg={result_info.get('resultMsg')}") + + orders = result.get("orderList", []) + print(f"✓ 获取到 {len(orders)} 笔交易") + return orders + +def main(): + api = PaytmBusinessAPI() + + # 示例账号 + mobile = "7528905079" + password = "Kular@500" + + try: + # 1. 尝试加载已保存的 token + if api.load_token(): + print("\n使用已保存的 Token") + else: + print("\n需要重新登录") + # 2. 初始化 + api.init() + + # 3. 请求 OTP + api.request_otp(mobile, password) + + # 4. 验证 OTP + otp = input("请输入收到的 OTP: ") + api.verify_otp(otp) + + # 5. 获取 QR 数据 + api.fetch_qr_data() + + # 6. 保存 token + api.save_token() + + # 7. 获取流水 + print("\n查询最近1天的交易流水...") + transactions = api.get_transactions() + + if transactions: + print(f"\n最新 5 笔交易:") + for i, txn in enumerate(transactions[:5], 1): + amount = float(txn.get("payMoneyAmount", {}).get("value", 0)) / 100 + customer = txn.get("additionalInfo", {}).get("customerName", "Unknown") + upi = txn.get("additionalInfo", {}).get("virtualPaymentAddr", "") + order_id = txn.get("bizOrderId", "") + print(f"{i}. {txn.get('orderCreatedTime')} | ₹{amount:.2f} | {customer} ({upi}) | {order_id}") + + txn_file = ".paytm_business_transactions.json" + with open(txn_file, 'w') as f: + json.dump(transactions, f, indent=2, ensure_ascii=False) + print(f"\n✓ {len(transactions)} 笔交易已保存到 {txn_file}") + else: + print("暂无交易记录") + + except Exception as e: + print(f"\n✗ 错误: {e}") + +if __name__ == "__main__": + main() diff --git a/servers/walletman b/servers/walletman index ae94e22..248df18 160000 --- a/servers/walletman +++ b/servers/walletman @@ -1 +1 @@ -Subproject commit ae94e22e384fde8734c6ec575c14c11bc89f724b +Subproject commit 248df18fcf005856ecc52195252b1a54dcb9fa22 diff --git a/services/api.ts b/services/api.ts index ed8d9d6..05efeed 100644 --- a/services/api.ts +++ b/services/api.ts @@ -62,7 +62,7 @@ class Api { const res = await fetch(`${Api.BASE_URL}/request-otp`, { method: 'POST', headers: this.headers(), - body: JSON.stringify({ walletType, mobile, ...params }), + body: JSON.stringify({ walletType, mobile, params }), }); const data = await res.json(); if (!data.success) throw new Error(data.message);