diff --git a/.gitignore b/.gitignore
index c43bfc5..e669e96 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,3 +17,4 @@ android/.idea/caches
ios/build
ios/Pods
android/app/release
+__pycache__
diff --git a/requirements-gui.txt b/requirements-gui.txt
new file mode 100644
index 0000000..5b97c7c
--- /dev/null
+++ b/requirements-gui.txt
@@ -0,0 +1 @@
+PySide6>=6.6.0
diff --git a/servers/walletman b/servers/walletman
index 7ee1c24..f551940 160000
--- a/servers/walletman
+++ b/servers/walletman
@@ -1 +1 @@
-Subproject commit 7ee1c24dd23207f6501709d0e2f572f18cfac22d
+Subproject commit f5519400a0c970cd984c138e059cbc8b3ade92b8
diff --git a/upi.html b/upi.html
index a0351df..ce706c9 100644
--- a/upi.html
+++ b/upi.html
@@ -140,6 +140,10 @@
background: linear-gradient(135deg, #00897b, #4db6ac);
}
+ .c-gpay {
+ background: linear-gradient(135deg, #1a73e8, #34a853);
+ }
+
.c-upi {
background: linear-gradient(135deg, #37474f, #78909c);
}
@@ -192,6 +196,7 @@
+
@@ -253,7 +258,7 @@
bharatpe_biz: {
label: 'BharatPe Business',
pa: 'BHARATPE.8A0T1R2O5E77327@fbpe', pn: 'BharatPe Merchant',
- extra: () => '&tn=Pay%20To%20BharatPe%20Merchant',
+ extra: () => '&tn=Pay+To+BharatPe+Merchant',
warn: null,
},
paytm_per: {
@@ -276,7 +281,7 @@
},
freecharge_per: {
label: 'Freecharge Personal',
- pa: 'simple6812@freecharge', pn: 'Gurvir Singh',
+ pa: '9124307439@freecharge', pn: 'Niranjan Mishra',
extra: () => '',
warn: null,
},
@@ -310,6 +315,7 @@
paytm: (pa, pn, am, extra) => `paytmmp://cash_wallet?featuretype=money_transfer&pa=${pa}&pn=${pn}&am=${am}&cu=INR&mc=0000&mode=02&purpose=00&orgid=159002${extra}`,
phonepe: (pa, pn, am) => phonepeNative(pa, pn, am, 'payment'),
phonepe_pay: (pa, pn, am, extra) => `phonepe://pay?pa=${pa}&pn=${pn}&am=${am}&cu=INR${extra}`,
+ gpay: (pa, pn, am, extra) => `tez://upi/pay?pa=${pa}&pn=${pn}&am=${am}&cu=INR${extra}`,
mobikwik: (pa, pn, am, extra) => { const tn = (extra.match(/[?&]tn=([^&]*)/) || [])[1] || 'payment'; return `mobikwik://moneytransfer/upi/verifyVpa?vpa=${encodeURIComponent(pa)}&amount=${am}¬e=${tn}`; },
freecharge: (pa, pn, am, extra) => `freechargeupi://pay?pa=${pa}&pn=${pn}&am=${am}&cu=INR${extra}`,
// fallback 在 scheduleAutoClick 里处理
@@ -318,7 +324,7 @@
const APP_COLORS = {
paytm: 'c-paytm', phonepe: 'c-phonepe', phonepe_pay: 'c-phonepe',
- mobikwik: 'c-mobikwik', freecharge: 'c-freecharge', upi: 'c-upi',
+ gpay: 'c-gpay', mobikwik: 'c-mobikwik', freecharge: 'c-freecharge', upi: 'c-upi',
};
const PHONEPE_WALLETS = new Set(['phonepe_biz', 'phonepe_per']);
diff --git a/walletsgui.py b/walletsgui.py
new file mode 100644
index 0000000..30407a4
--- /dev/null
+++ b/walletsgui.py
@@ -0,0 +1,423 @@
+#!/usr/bin/env python3
+"""wallets.json 图形管理(PySide6)。默认同目录下 servers/walletman/cmd/wallets.json。"""
+
+from __future__ import annotations
+
+import json
+import sys
+from pathlib import Path
+from typing import Any
+
+from PySide6.QtCore import Qt
+from PySide6.QtGui import QAction, QCloseEvent, QKeySequence
+from PySide6.QtWidgets import (
+ QApplication,
+ QComboBox,
+ QDialog,
+ QDialogButtonBox,
+ QFileDialog,
+ QFormLayout,
+ QHBoxLayout,
+ QHeaderView,
+ QLabel,
+ QLineEdit,
+ QMainWindow,
+ QMessageBox,
+ QPushButton,
+ QTableWidget,
+ QTableWidgetItem,
+ QTextEdit,
+ QVBoxLayout,
+ QWidget,
+)
+
+# 与 servers/walletman/cmd/tools/main.go 中 walletTypeMenu 一致
+WALLET_TYPES = [
+ "paytm",
+ "phonepe",
+ "mobikwik",
+ "freecharge",
+ "bharatpe business",
+ "paytm business",
+ "phonepe business",
+ "googlepay business",
+]
+
+
+def default_wallets_path() -> Path:
+ root = Path(__file__).resolve().parent
+ for rel in (
+ root / "servers" / "walletman" / "cmd" / "wallets.json",
+ root / "wallets.json",
+ ):
+ if rel.is_file():
+ return rel
+ return root / "servers" / "walletman" / "cmd" / "wallets.json"
+
+
+def parse_params(s: str) -> dict[str, Any]:
+ s = s.strip()
+ if not s:
+ raise ValueError("params 为空")
+ return json.loads(s)
+
+
+def format_params_pretty(s: str) -> str:
+ s = s.strip()
+ if not s:
+ return ""
+ return json.dumps(json.loads(s), ensure_ascii=False, indent=2)
+
+
+def compact_params_json(s: str) -> str:
+ return json.dumps(json.loads(s), ensure_ascii=False, separators=(",", ":"))
+
+
+def suggest_wallet_id(wallet_type: str, params: str) -> str:
+ try:
+ p = json.loads(params)
+ except json.JSONDecodeError:
+ return ""
+ mob = p.get("mobile")
+ if isinstance(mob, str) and mob:
+ return f"{wallet_type}_{mob}"
+ return ""
+
+
+class WalletEditDialog(QDialog):
+ def __init__(
+ self,
+ parent: QWidget | None,
+ title: str,
+ wallet_type: str,
+ wallet_id: str,
+ params: str,
+ allow_change_id: bool = True,
+ *,
+ is_add: bool = False,
+ ) -> None:
+ super().__init__(parent)
+ self.setWindowTitle(title)
+ self.setMinimumSize(640, 480)
+ self._is_add = is_add
+ self._id_locked = not allow_change_id and bool(wallet_id)
+
+ layout = QVBoxLayout(self)
+ form = QFormLayout()
+ self.edit_uid: QLineEdit | None = None
+ if is_add:
+ self.edit_uid = QLineEdit("10000")
+ self.edit_uid.setPlaceholderText("如 10000")
+ form.addRow("用户", self.edit_uid)
+ self.combo_type = QComboBox()
+ self.combo_type.setEditable(True)
+ for t in WALLET_TYPES:
+ self.combo_type.addItem(t)
+ idx = self.combo_type.findText(wallet_type, Qt.MatchFlag.MatchFixedString)
+ if idx >= 0:
+ self.combo_type.setCurrentIndex(idx)
+ else:
+ self.combo_type.setCurrentText(wallet_type)
+ self.edit_id = QLineEdit(wallet_id)
+ self.edit_id.setPlaceholderText("留空则根据 params 里 mobile 自动生成")
+ if self._id_locked:
+ self.edit_id.setReadOnly(True)
+ form.addRow("类型", self.combo_type)
+ form.addRow("钱包 key", self.edit_id)
+ layout.addLayout(form)
+ self.text_params = QTextEdit()
+ self.text_params.setFontFamily("Menlo" if sys.platform == "darwin" else "Consolas")
+ self.text_params.setPlainText(params)
+ layout.addWidget(QLabel("params (JSON)"))
+ layout.addWidget(self.text_params, 1)
+ box = QDialogButtonBox(
+ QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
+ )
+ box.accepted.connect(self._accept)
+ box.rejected.connect(self.reject)
+ layout.addWidget(box)
+
+ def _accept(self) -> None:
+ ptxt = self.text_params.toPlainText().strip()
+ try:
+ parse_params(ptxt)
+ except (json.JSONDecodeError, ValueError) as e:
+ QMessageBox.warning(self, "JSON 无效", str(e))
+ return
+ self._ok = True
+ self.accept()
+
+ @property
+ def user_id(self) -> str:
+ if not self._is_add or self.edit_uid is None:
+ return "10000"
+ return self.edit_uid.text().strip() or "10000"
+ return self.combo_type.currentText().strip()
+
+ @property
+ def wallet_id(self) -> str:
+ return self.edit_id.text().strip()
+
+ @property
+ def params_compact(self) -> str:
+ return compact_params_json(self.text_params.toPlainText())
+
+
+class WalletsWindow(QMainWindow):
+ COL_USER, COL_ID, COL_TYPE, COL_PREVIEW = 0, 1, 2, 3
+
+ def __init__(self) -> None:
+ super().__init__()
+ self.setWindowTitle("Wallets 管理器")
+ self.resize(1100, 600)
+ self._path: Path = default_wallets_path()
+ self._data: dict[str, dict[str, dict[str, str]]] = {}
+ self._dirty = False
+
+ central = QWidget()
+ self.setCentralWidget(central)
+ v = QVBoxLayout(central)
+ path_row = QHBoxLayout()
+ self.label_path = QLabel()
+ path_row.addWidget(self.label_path)
+ btn_browse = QPushButton("打开…")
+ btn_browse.clicked.connect(self._open_file)
+ path_row.addWidget(btn_browse)
+ v.addLayout(path_row)
+
+ self.table = QTableWidget(0, 4)
+ self.table.setHorizontalHeaderLabels(["用户", "Wallet ID", "类型", "params 摘要"])
+ self.table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.Stretch)
+ self.table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
+ self.table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
+ self.table.doubleClicked.connect(self._on_double_click)
+ v.addWidget(self.table)
+
+ row = QHBoxLayout()
+ for text, fn in [
+ ("重新加载", self._reload),
+ ("保存", self._save),
+ ("添加", self._add),
+ ("编辑", self._edit),
+ ("删除", self._delete),
+ ]:
+ b = QPushButton(text)
+ b.clicked.connect(fn)
+ row.addWidget(b)
+ v.addLayout(row)
+ self._update_path_label()
+ self._build_menu()
+ self.statusBar().showMessage("就绪")
+ self._load_file(self._path, quiet=True)
+
+ def _build_menu(self) -> None:
+ m = self.menuBar().addMenu("文件")
+ a_open = QAction("打开…", self)
+ a_open.setShortcut(QKeySequence.StandardKey.Open)
+ a_open.triggered.connect(self._open_file)
+ m.addAction(a_open)
+ a_save = QAction("保存", self)
+ a_save.setShortcut(QKeySequence.StandardKey.Save)
+ a_save.triggered.connect(self._save)
+ m.addAction(a_save)
+ m.addSeparator()
+ a_exit = QAction("退出", self)
+ a_exit.setShortcut(QKeySequence.StandardKey.Quit)
+ a_exit.triggered.connect(self.close)
+ m.addAction(a_exit)
+
+ def _update_path_label(self) -> None:
+ self.label_path.setText(f"文件: {self._path} " + ("● 未保存" if self._dirty else ""))
+ self.label_path.setTextInteractionFlags(
+ Qt.TextInteractionFlag.TextSelectableByMouse
+ )
+
+ def _set_dirty(self, d: bool) -> None:
+ self._dirty = d
+ self._update_path_label()
+
+ def _load_file(self, path: Path, quiet: bool = False) -> None:
+ if not path.is_file():
+ self._data = {}
+ self._rebuild_table()
+ self._set_dirty(False)
+ if not quiet:
+ QMessageBox.information(self, "提示", f"文件不存在,将使用空数据:\n{path}")
+ return
+ try:
+ raw = path.read_text(encoding="utf-8")
+ self._data = json.loads(raw)
+ except (OSError, json.JSONDecodeError) as e:
+ QMessageBox.critical(self, "读取失败", str(e))
+ return
+ self._path = path.resolve()
+ self._rebuild_table()
+ self._set_dirty(False)
+ self.setWindowTitle(f"Wallets 管理器 — {self._path.name}")
+
+ def _reload(self) -> None:
+ if self._dirty:
+ r = QMessageBox.question(
+ self, "未保存", "是否丢弃未保存的修改?",
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
+ )
+ if r != QMessageBox.StandardButton.Yes:
+ return
+ self._load_file(self._path, quiet=True)
+
+ def _save(self) -> None:
+ self._path.parent.mkdir(parents=True, exist_ok=True)
+ try:
+ text = json.dumps(self._data, ensure_ascii=False, indent=2) + "\n"
+ self._path.write_text(text, encoding="utf-8")
+ except OSError as e:
+ QMessageBox.critical(self, "保存失败", str(e))
+ return
+ self._set_dirty(False)
+ QMessageBox.information(self, "已保存", str(self._path))
+
+ def _open_file(self) -> None:
+ if self._dirty:
+ r = QMessageBox.question(
+ self, "未保存", "是否先保存?",
+ QMessageBox.StandardButton.Save
+ | QMessageBox.StandardButton.Discard
+ | QMessageBox.StandardButton.Cancel,
+ )
+ if r == QMessageBox.StandardButton.Save:
+ self._save()
+ elif r == QMessageBox.StandardButton.Cancel:
+ return
+ path, _ = QFileDialog.getOpenFileName(
+ self, "选择 wallets.json", str(self._path.parent), "JSON (*.json);;*.*"
+ )
+ if path:
+ self._load_file(Path(path))
+
+ def _rebuild_table(self) -> None:
+ rows: list[tuple[str, str, str, str]] = []
+ for uid, wallets in self._data.items():
+ for wid, ent in wallets.items():
+ wt = ent.get("walletType", "")
+ p = ent.get("params", "")
+ prev = p[:80] + ("…" if len(p) > 80 else "")
+ rows.append((str(uid), wid, str(wt), prev))
+ rows.sort(key=lambda x: (x[0], x[1]))
+ self.table.setRowCount(len(rows))
+ for i, (uid, wid, wt, prev) in enumerate(rows):
+ self.table.setItem(i, self.COL_USER, QTableWidgetItem(uid))
+ self.table.setItem(i, self.COL_ID, QTableWidgetItem(wid))
+ self.table.setItem(i, self.COL_TYPE, QTableWidgetItem(wt))
+ self.table.setItem(i, self.COL_PREVIEW, QTableWidgetItem(prev))
+
+ def _selected_cell(self) -> tuple[int, int] | None:
+ idx = self.table.currentIndex()
+ if not idx.isValid():
+ return None
+ return idx.row(), idx.column()
+
+ def _row_to_keys(self, row: int) -> tuple[str, str] | None:
+ u = self.table.item(row, self.COL_USER)
+ w = self.table.item(row, self.COL_ID)
+ if not u or not w:
+ return None
+ return u.text(), w.text()
+
+ def _on_double_click(self) -> None:
+ self._edit()
+
+ def _add(self) -> None:
+ default_type = WALLET_TYPES[0]
+ dlg = WalletEditDialog(self, "添加钱包", default_type, "", "{}", allow_change_id=True, is_add=True)
+ if dlg.exec() != QDialog.DialogCode.Accepted:
+ return
+ uid = dlg.user_id
+ wt, pid, pcompact = dlg.wallet_type, dlg.wallet_id, dlg.params_compact
+ if not pid:
+ pid = suggest_wallet_id(wt, pcompact)
+ if not pid:
+ QMessageBox.warning(self, "错误", "无法生成 Wallet ID,请填写「钱包 key」或确保 params 含 mobile")
+ return
+ if uid not in self._data:
+ self._data[uid] = {}
+ existed = pid in self._data[uid]
+ self._data[uid][pid] = {"walletType": wt, "params": pcompact}
+ self._rebuild_table()
+ self._set_dirty(True)
+ if existed:
+ self.statusBar().showMessage(f"已更新(原记录已存在): {uid} / {pid}", 5000)
+ else:
+ self.statusBar().showMessage(f"已添加: {uid} / {pid}", 5000)
+
+ def _edit(self) -> None:
+ sel = self._selected_cell()
+ if not sel:
+ QMessageBox.information(self, "提示", "先选中一行")
+ return
+ row = sel[0]
+ keys = self._row_to_keys(row)
+ if not keys:
+ return
+ uid, wid = keys
+ ent = self._data.get(uid, {}).get(wid)
+ if not ent:
+ return
+ wt = ent["walletType"]
+ try:
+ pretty = format_params_pretty(ent["params"])
+ except (json.JSONDecodeError, TypeError):
+ pretty = str(ent.get("params", ""))
+ dlg = WalletEditDialog(self, "编辑钱包", wt, wid, pretty, allow_change_id=False, is_add=False)
+ if dlg.exec() != QDialog.DialogCode.Accepted:
+ return
+ nwt, _, pcompact = dlg.wallet_type, dlg.wallet_id, dlg.params_compact
+ self._data[uid][wid] = {"walletType": nwt, "params": pcompact}
+ self._rebuild_table()
+ self._set_dirty(True)
+
+ def _delete(self) -> None:
+ sel = self._selected_cell()
+ if not sel:
+ return
+ keys = self._row_to_keys(sel[0])
+ if not keys:
+ return
+ uid, wid = keys
+ if (
+ QMessageBox.question(
+ self, "删除", f"确定删除 {uid} / {wid} ?",
+ )
+ != QMessageBox.StandardButton.Yes
+ ):
+ return
+ self._data.get(uid, {}).pop(wid, None)
+ if self._data.get(uid) == {}:
+ self._data.pop(uid, None)
+ self._rebuild_table()
+ self._set_dirty(True)
+
+ def closeEvent(self, event: QCloseEvent) -> None:
+ if self._dirty:
+ r = QMessageBox.question(
+ self, "未保存", "是否保存后退出?",
+ QMessageBox.StandardButton.Save
+ | QMessageBox.StandardButton.Discard
+ | QMessageBox.StandardButton.Cancel,
+ )
+ if r == QMessageBox.StandardButton.Save:
+ self._save()
+ elif r == QMessageBox.StandardButton.Cancel:
+ event.ignore()
+ return
+ event.accept()
+
+
+def main() -> None:
+ app = QApplication(sys.argv)
+ w = WalletsWindow()
+ w.show()
+ sys.exit(app.exec())
+
+
+if __name__ == "__main__":
+ main()