This commit is contained in:
2026-09-15 15:49:47 +08:00
parent 376458c45c
commit e7e6d95044
4 changed files with 506 additions and 1 deletions
+59
View File
@@ -0,0 +1,59 @@
{
"payin": {
"default": [
{
"from_sec": 0,
"to_sec": 60,
"interval_sec": 20
},
{
"from_sec": 60,
"to_sec": 600,
"interval_sec": 20
}
],
"paytm": [
{
"from_sec": 0,
"to_sec": 60,
"interval_sec": 20
},
{
"from_sec": 60,
"to_sec": 600,
"interval_sec": 20
}
]
},
"payout": {
"default": [
{
"from_sec": 0,
"to_sec": 60,
"interval_sec": 20
},
{
"from_sec": 60,
"to_sec": 1200,
"interval_sec": 20
}
],
"paytm": [
{
"from_sec": 0,
"to_sec": 60,
"interval_sec": 20
},
{
"from_sec": 60,
"to_sec": 1200,
"interval_sec": 20
}
]
},
"payin_first_delay_sec": 20,
"payin_check_delay_sec": 20,
"payout_first_delay_sec": 20,
"payout_check_delay_sec": 20,
"check_processing_timeout_sec": 35
}
+413
View File
@@ -0,0 +1,413 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>钱包配置</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#f4f6f8;color:#222;min-height:100vh;padding:20px 16px 40px}
.wrap{max-width:920px;margin:0 auto}
h1{font-size:20px;margin-bottom:4px}
.sub{font-size:12px;color:#666;margin-bottom:16px}
.toolbar{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:16px}
.toolbar input,.toolbar select{padding:8px 10px;border:1px solid #ddd;border-radius:8px;font-size:13px;background:#fff}
.toolbar input{min-width:160px}
.btn{padding:8px 14px;border:none;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;background:#2563eb;color:#fff}
.btn:hover{opacity:.92}
.btn.secondary{background:#fff;color:#333;border:1px solid #ddd}
.btn.danger{background:#dc2626}
.btn.sm{padding:5px 10px;font-size:12px}
.card{background:#fff;border-radius:12px;padding:16px;margin-bottom:12px;box-shadow:0 2px 8px rgba(0,0,0,.06)}
.card-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:12px}
.wallet-name{font-size:16px;font-weight:700}
.wallet-actions{display:flex;gap:6px;flex-wrap:wrap}
.mode-block{border:1px solid #eee;border-radius:10px;padding:12px;margin-bottom:8px;background:#fafafa}
.mode-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}
.mode-tag{font-size:12px;font-weight:700;padding:3px 8px;border-radius:6px;background:#dbeafe;color:#1d4ed8;text-transform:uppercase}
.mode-tag.otp{background:#fef3c7;color:#b45309}
.toggles{display:grid;grid-template-columns:1fr 1fr;gap:10px}
.toggle{display:flex;align-items:center;justify-content:space-between;padding:10px 12px;background:#fff;border:1px solid #e5e7eb;border-radius:8px}
.toggle label{font-size:13px;font-weight:600;color:#444}
.switch{position:relative;width:44px;height:24px;flex-shrink:0}
.switch input{opacity:0;width:0;height:0}
.slider{position:absolute;inset:0;background:#cbd5e1;border-radius:999px;cursor:pointer;transition:.2s}
.slider:before{content:'';position:absolute;width:18px;height:18px;left:3px;top:3px;background:#fff;border-radius:50%;transition:.2s}
.switch input:checked+.slider{background:#22c55e}
.switch input:checked+.slider:before{transform:translateX(20px)}
.switch input:disabled+.slider{background:#e5e7eb;cursor:not-allowed;opacity:.75}
.switch input:disabled:checked+.slider{background:#86efac;opacity:.55}
.wallet-switches{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-bottom:14px;padding:12px;background:#f8fafc;border:1px solid #e5e7eb;border-radius:10px}
.wallet-switches .toggle{background:#fff}
.wallet-switches .toggle.disabled{opacity:.55}
.wallet-switches .toggle.disabled label{color:#94a3b8}
.card.off{opacity:.72}
.card.off .wallet-name{color:#64748b}
.empty{text-align:center;padding:40px 16px;color:#888;font-size:14px;border:1px dashed #ddd;border-radius:12px;background:#fff}
.add-mode{display:flex;gap:8px;margin-top:8px}
.add-mode select{flex:1;padding:8px;border:1px solid #ddd;border-radius:8px}
.whitelist{margin-top:12px;padding-top:12px;border-top:1px dashed #e5e7eb}
.whitelist-title{font-size:12px;font-weight:700;color:#555;margin-bottom:8px}
.whitelist-hint{font-size:11px;color:#888;margin-bottom:8px}
.whitelist-tags{display:flex;flex-wrap:wrap;gap:6px;min-height:28px;margin-bottom:8px}
.tag{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;background:#eef2ff;color:#3730a3;border-radius:999px;font-size:12px;font-weight:600}
.tag button{border:none;background:transparent;color:#6366f1;cursor:pointer;font-size:14px;line-height:1;padding:0}
.tag.empty-tag{background:#f3f4f6;color:#888;font-weight:500}
.whitelist-add{display:flex;gap:8px}
.whitelist-add input{flex:1;padding:8px 10px;border:1px solid #ddd;border-radius:8px;font-size:13px}
.json-box{margin-top:16px}
.json-box textarea{width:100%;min-height:160px;padding:12px;border:1px solid #ddd;border-radius:10px;font-family:ui-monospace,monospace;font-size:12px;line-height:1.5;resize:vertical}
.toast{position:fixed;top:16px;right:16px;padding:10px 14px;border-radius:8px;color:#fff;font-size:13px;z-index:99;opacity:0;transform:translateY(-8px);transition:.25s;pointer-events:none}
.toast.show{opacity:1;transform:none}
.toast.ok{background:#16a34a}.toast.err{background:#dc2626}
</style>
</head>
<body>
<div class="wrap">
<h1>钱包配置</h1>
<p class="sub">钱包总开关 · 代收/代付调度 · app/otp · payin/payout · userId 白名单</p>
<div class="toolbar">
<input id="newWalletName" placeholder="新钱包名,如 paytm">
<button class="btn" onclick="addWallet()">添加钱包</button>
<button class="btn secondary" onclick="importJson()">导入 JSON</button>
<button class="btn secondary" onclick="exportJson()">导出 JSON</button>
<button class="btn secondary" onclick="copyJson()">复制 JSON</button>
<input type="file" id="fileInput" accept=".json,application/json" hidden>
</div>
<div id="walletList"></div>
<div id="emptyHint" class="empty" style="display:none">暂无钱包,上方添加</div>
<div class="card json-box">
<div class="card-head"><strong>JSON 预览</strong></div>
<textarea id="jsonPreview" readonly></textarea>
</div>
</div>
<div id="toast" class="toast"></div>
<script>
const DEFAULT_CONFIG = {
"paytm": { "app": { "payin": true, "payout": true }, "otp": { "payin": true, "payout": false } },
"phonepe": { "app": { "payin": true, "payout": true }, "otp": { "payin": true, "payout": false } },
"mobikwik": { "app": { "payin": true, "payout": true }, "otp": { "payin": true, "payout": true } },
"freecharge": { "otp": { "payin": true, "payout": true } },
"amazonpay": { "app": { "payin": true, "payout": true } },
"bhartepe business": { "app": { "payin": true, "payout": false } },
"googlepay business": { "app": { "payin": true, "payout": false } },
"paytm business": { "app": { "payin": true, "payout": false }, "otp": { "payin": true, "payout": false } },
"phonepe business": { "app": { "payin": true, "payout": false } }
};
let config = structuredClone(DEFAULT_CONFIG);
const MODES = ['app', 'otp'];
const FLAGS = ['payin', 'payout'];
function toast(msg, type = 'ok') {
const el = document.getElementById('toast');
el.textContent = msg;
el.className = 'toast show ' + type;
clearTimeout(el._t);
el._t = setTimeout(() => el.classList.remove('show'), 2200);
}
function normalizeUserIds(list) {
if (!Array.isArray(list)) return [];
const seen = new Set();
const out = [];
for (const item of list) {
const id = String(item).trim();
if (!/^\d+$/.test(id) || seen.has(id)) continue;
seen.add(id);
out.push(Number(id));
}
return out.sort((a, b) => a - b);
}
function hasPayinCapability(w) {
return MODES.some(m => w[m]?.payin);
}
function hasPayoutCapability(w) {
return MODES.some(m => w[m]?.payout);
}
function ensureWalletMeta(w) {
if (w.enabled === undefined) w.enabled = true;
if (w.payinDispatch === undefined) w.payinDispatch = true;
if (w.payoutDispatch === undefined) w.payoutDispatch = false;
if (!hasPayoutCapability(w)) w.payoutDispatch = false;
if (!hasPayinCapability(w)) w.payinDispatch = false;
}
function normalizeWallet(w) {
const out = {};
out.enabled = w.enabled !== false;
out.payinDispatch = !!w.payinDispatch;
out.payoutDispatch = !!w.payoutDispatch;
const whitelist = normalizeUserIds(w.userIdWhitelist);
if (whitelist.length) out.userIdWhitelist = whitelist;
for (const mode of MODES) {
if (!w[mode]) continue;
out[mode] = { payin: !!w[mode].payin, payout: !!w[mode].payout };
}
if (!hasPayoutCapability(out)) out.payoutDispatch = false;
if (!hasPayinCapability(out)) out.payinDispatch = false;
return out;
}
function normalizeConfig(raw) {
const out = {};
for (const [name, wallet] of Object.entries(raw || {})) {
const n = normalizeWallet(wallet);
const hasModes = MODES.some(m => n[m]);
if (hasModes || n.userIdWhitelist?.length) out[name] = n;
}
return out;
}
function walletModes(w) {
return MODES.filter(m => w[m]);
}
function render() {
const list = document.getElementById('walletList');
const names = Object.keys(config);
document.getElementById('emptyHint').style.display = names.length ? 'none' : 'block';
list.innerHTML = names.map(name => {
const w = config[name];
ensureWalletMeta(w);
const modes = walletModes(w);
const canPayin = hasPayinCapability(w);
const canPayout = hasPayoutCapability(w);
const masterHtml = `
<div class="wallet-switches">
<div class="toggle">
<label>钱包可用</label>
<label class="switch">
<input type="checkbox" ${w.enabled ? 'checked' : ''} onchange="setWalletMeta('${esc(name)}','enabled',this.checked)">
<span class="slider"></span>
</label>
</div>
<div class="toggle ${canPayin ? '' : 'disabled'}">
<label>代收调度</label>
<label class="switch">
<input type="checkbox" ${w.payinDispatch ? 'checked' : ''} ${canPayin ? '' : 'disabled'}
onchange="setWalletMeta('${esc(name)}','payinDispatch',this.checked)">
<span class="slider"></span>
</label>
</div>
<div class="toggle ${canPayout ? '' : 'disabled'}">
<label>代付调度</label>
<label class="switch">
<input type="checkbox" ${w.payoutDispatch ? 'checked' : ''} ${canPayout ? '' : 'disabled'}
onchange="setWalletMeta('${esc(name)}','payoutDispatch',this.checked)">
<span class="slider"></span>
</label>
</div>
</div>`;
const whitelist = w.userIdWhitelist || [];
const whitelistHtml = `
<div class="whitelist">
<div class="whitelist-title">userId 白名单</div>
<div class="whitelist-hint">留空表示不限制;有值时仅 listed userId 可用该钱包</div>
<div class="whitelist-tags">
${whitelist.length ? whitelist.map(uid => `
<span class="tag">${esc(uid)}<button type="button" onclick="removeUserId('${esc(name)}','${esc(uid)}')" title="移除">×</button></span>
`).join('') : '<span class="tag empty-tag">未限制</span>'}
</div>
<div class="whitelist-add">
<input id="uid_${css(name)}" placeholder="userId,如 10000" inputmode="numeric" pattern="\\d*"
onkeydown="if(event.key==='Enter'){event.preventDefault();addUserId('${esc(name)}')}">
<button class="btn sm secondary" type="button" onclick="addUserId('${esc(name)}')">添加</button>
</div>
</div>`;
const modeHtml = modes.map(mode => `
<div class="mode-block">
<div class="mode-head">
<span class="mode-tag ${mode}">${mode}</span>
<button class="btn sm danger" onclick="removeMode('${esc(name)}','${mode}')">删除模式</button>
</div>
<div class="toggles">
${FLAGS.map(flag => `
<div class="toggle">
<label>${flag}</label>
<label class="switch">
<input type="checkbox" ${w[mode][flag] ? 'checked' : ''}
onchange="setFlag('${esc(name)}','${mode}','${flag}',this.checked)">
<span class="slider"></span>
</label>
</div>`).join('')}
</div>
</div>`).join('');
const missing = MODES.filter(m => !w[m]);
const addModeHtml = missing.length ? `
<div class="add-mode">
<select id="addMode_${css(name)}">${missing.map(m => `<option value="${m}">${m}</option>`).join('')}</select>
<button class="btn sm secondary" onclick="addMode('${esc(name)}')">添加模式</button>
</div>` : '';
return `<div class="card ${w.enabled ? '' : 'off'}" data-wallet="${esc(name)}">
<div class="card-head">
<div class="wallet-name">${esc(name)}</div>
<div class="wallet-actions">
<button class="btn sm secondary" onclick="renameWallet('${esc(name)}')">重命名</button>
<button class="btn sm danger" onclick="removeWallet('${esc(name)}')">删除钱包</button>
</div>
</div>
${masterHtml}
${modeHtml || '<div style="color:#888;font-size:13px;margin-bottom:8px">暂无模式</div>'}
${addModeHtml}
${whitelistHtml}
</div>`;
}).join('');
document.getElementById('jsonPreview').value = JSON.stringify(exportConfig(), null, 4);
}
function exportConfig() {
const out = {};
for (const [name, wallet] of Object.entries(config)) {
out[name] = normalizeWallet(wallet);
}
return out;
}
function esc(s) { return String(s).replace(/\\/g, '\\\\').replace(/'/g, "\\'"); }
function css(s) { return String(s).replace(/[^a-zA-Z0-9_-]/g, '_'); }
function addWallet() {
const input = document.getElementById('newWalletName');
const name = input.value.trim();
if (!name) return toast('请输入钱包名', 'err');
if (config[name]) return toast('钱包已存在', 'err');
config[name] = { enabled: true, payinDispatch: true, payoutDispatch: false, app: { payin: true, payout: false }, userIdWhitelist: [] };
input.value = '';
render();
toast('已添加 ' + name);
}
function removeWallet(name) {
if (!confirm('删除钱包「' + name + '」?')) return;
delete config[name];
render();
}
function renameWallet(name) {
const next = prompt('新名称', name);
if (!next || next.trim() === name) return;
const n = next.trim();
if (config[n]) return toast('名称已存在', 'err');
config[n] = config[name];
delete config[name];
render();
}
function addMode(name) {
const sel = document.getElementById('addMode_' + css(name));
const mode = sel.value;
if (config[name][mode]) return toast('模式已存在', 'err');
config[name][mode] = { payin: true, payout: false };
render();
toast('已添加 ' + mode);
}
function removeMode(name, mode) {
if (!confirm('删除「' + name + '」的 ' + mode + ' 模式?')) return;
delete config[name][mode];
if (!walletModes(config[name]).length && !(config[name].userIdWhitelist || []).length) delete config[name];
render();
}
function ensureWhitelist(name) {
if (!config[name].userIdWhitelist) config[name].userIdWhitelist = [];
}
function addUserId(name) {
const input = document.getElementById('uid_' + css(name));
const id = (input?.value || '').trim();
if (!/^\d+$/.test(id)) return toast('userId 必须是数字', 'err');
ensureWhitelist(name);
const numId = Number(id);
if (config[name].userIdWhitelist.includes(numId)) return toast('userId 已在白名单', 'err');
config[name].userIdWhitelist.push(numId);
config[name].userIdWhitelist = normalizeUserIds(config[name].userIdWhitelist);
if (input) input.value = '';
render();
toast('已添加 userId ' + id);
}
function removeUserId(name, uid) {
ensureWhitelist(name);
config[name].userIdWhitelist = config[name].userIdWhitelist.filter(x => x !== Number(uid));
if (!config[name].userIdWhitelist.length) delete config[name].userIdWhitelist;
render();
}
function setWalletMeta(name, key, val) {
if (!config[name]) return;
config[name][key] = val;
ensureWalletMeta(config[name]);
render();
}
function setFlag(name, mode, flag, val) {
if (!config[name]?.[mode]) return;
config[name][mode][flag] = val;
ensureWalletMeta(config[name]);
render();
}
function exportJson() {
const blob = new Blob([JSON.stringify(exportConfig(), null, 4)], { type: 'application/json' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'wallets_config.json';
a.click();
URL.revokeObjectURL(a.href);
toast('已导出');
}
function copyJson() {
navigator.clipboard.writeText(JSON.stringify(exportConfig(), null, 4)).then(() => toast('已复制'));
}
function importJson() {
document.getElementById('fileInput').click();
}
document.getElementById('fileInput').addEventListener('change', e => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = () => {
try {
config = normalizeConfig(JSON.parse(reader.result));
Object.values(config).forEach(ensureWalletMeta);
render();
toast('导入成功');
} catch {
toast('JSON 格式错误', 'err');
}
e.target.value = '';
};
reader.readAsText(file);
});
async function tryLoadLocal() {
try {
const res = await fetch('wallets_config.json?_=' + Date.now());
if (res.ok) {
config = normalizeConfig(await res.json());
Object.values(config).forEach(ensureWalletMeta);
}
} catch (_) {}
Object.values(config).forEach(ensureWalletMeta);
render();
}
tryLoadLocal();
</script>
</body>
</html>
+33
View File
@@ -0,0 +1,33 @@
{
"paytm": {
"app" : { "payin" : true, "payout" : true },
"otp" : { "payin" : true, "payout" : false }
},
"phonepe": {
"app" : { "payin" : true, "payout" : true },
"otp" : { "payin" : true, "payout" : false }
},
"mobikwik": {
"app" : { "payin" : true, "payout" : true },
"otp" : { "payin" : true, "payout" : true }
},
"freecharge": {
"otp" : { "payin" : true, "payout" : true }
},
"amazonpay": {
"app" : { "payin" : true, "payout" : true }
},
"bhartepe business": {
"app" : { "payin" : true, "payout" : false }
},
"googlepay business": {
"app" : { "payin" : true, "payout" : false }
},
"paytm business": {
"app" : { "payin" : true, "payout" : false },
"otp" : { "payin" : true, "payout" : false }
},
"phonepe business": {
"app" : { "payin" : true, "payout" : false }
}
}