This commit is contained in:
2026-06-01 02:29:37 +08:00
parent eea21cd33b
commit 3352a3c865
3 changed files with 1 additions and 443 deletions

View File

@@ -22,6 +22,7 @@
<set> <set>
<option value="$PROJECT_DIR$" /> <option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" /> <option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/../node_modules/@eabdullazyanov/react-native-sms-user-consent/android" />
<option value="$PROJECT_DIR$/../node_modules/@react-native-async-storage/async-storage/android" /> <option value="$PROJECT_DIR$/../node_modules/@react-native-async-storage/async-storage/android" />
<option value="$PROJECT_DIR$/../node_modules/@react-native-cookies/cookies/android" /> <option value="$PROJECT_DIR$/../node_modules/@react-native-cookies/cookies/android" />
<option value="$PROJECT_DIR$/../node_modules/@react-native-ml-kit/barcode-scanning/android" /> <option value="$PROJECT_DIR$/../node_modules/@react-native-ml-kit/barcode-scanning/android" />

View File

@@ -1 +0,0 @@
PySide6>=6.6.0

View File

@@ -1,442 +0,0 @@
#!/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,
QSizePolicy,
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}"
# googlepay business: merchantInfo.phone 或 channelUid
merchant_info = p.get("merchantInfo") or {}
phone = merchant_info.get("phone") or p.get("phone")
if isinstance(phone, str) and phone:
return f"{wallet_type}_{phone}"
channel_uid = p.get("channelUid")
if isinstance(channel_uid, str) and channel_uid:
return f"{wallet_type}_{channel_uid}"
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(900, 480)
self.resize(1000, 600)
self._is_add = is_add
self._id_locked = not allow_change_id and bool(wallet_id)
layout = QVBoxLayout(self)
form = QFormLayout()
form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow)
form.setLabelAlignment(Qt.AlignmentFlag.AlignRight)
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 自动生成")
self.edit_id.setMinimumWidth(720)
sp = self.edit_id.sizePolicy()
sp.setHorizontalPolicy(QSizePolicy.Policy.Expanding)
self.edit_id.setSizePolicy(sp)
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"
@property
def wallet_type(self) -> str:
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()