@seasonkoh/webaz 0.1.28 → 0.1.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/README.zh-CN.md +7 -6
- package/dist/bond-refund-blockers.js +50 -0
- package/dist/bond-slash.js +100 -0
- package/dist/bond-terms.js +31 -0
- package/dist/currency.js +16 -0
- package/dist/deposit-rails.js +52 -0
- package/dist/direct-pay-aml-monitor.js +67 -0
- package/dist/direct-pay-aml-review.js +40 -0
- package/dist/direct-pay-base-bond-entry.js +5 -0
- package/dist/direct-pay-bond-rail-clearance.js +99 -0
- package/dist/direct-pay-cancel-refund.js +160 -0
- package/dist/direct-pay-compliance-ingress.js +145 -0
- package/dist/direct-pay-controls.js +136 -0
- package/dist/direct-pay-create.js +231 -0
- package/dist/direct-pay-deferral-quota.js +43 -0
- package/dist/direct-pay-disclosures.js +50 -0
- package/dist/direct-pay-eligibility.js +46 -0
- package/dist/direct-pay-fee-ar.js +241 -0
- package/dist/direct-pay-fee-prepay-request.js +80 -0
- package/dist/direct-pay-launch-readiness.js +108 -0
- package/dist/direct-pay-launch-summary.js +68 -0
- package/dist/direct-pay-ledger.js +94 -0
- package/dist/direct-pay-returns.js +104 -0
- package/dist/direct-pay-stock.js +9 -0
- package/dist/direct-receive-account-qr.js +77 -0
- package/dist/direct-receive-accounts.js +82 -0
- package/dist/direct-receive-deferral.js +171 -0
- package/dist/direct-receive-deposits.js +365 -0
- package/dist/direct-receive-payment-instruction.js +26 -0
- package/dist/free-shipping.js +37 -0
- package/dist/fx-rates.js +78 -0
- package/dist/layer0-foundation/L0-1-database/schema.js +777 -1
- package/dist/layer0-foundation/L0-2-state-machine/engine.js +1 -0
- package/dist/layer0-foundation/L0-2-state-machine/genuine-sale.js +15 -5
- package/dist/layer0-foundation/L0-2-state-machine/transitions.js +114 -0
- package/dist/layer0-foundation/L0-5-manifest/manifest.js +1 -1
- package/dist/layer1-agent/L1-1-mcp-server/cli.js +64 -0
- package/dist/layer1-agent/L1-1-mcp-server/network-mode.js +11 -0
- package/dist/layer1-agent/L1-1-mcp-server/server.js +99 -32
- package/dist/layer2-business/L2-6-notifications/notification-engine.js +110 -41
- package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +133 -31
- package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +16 -4
- package/dist/layer3-trust/L3-1-dispute-engine/mutual-cancel.js +156 -0
- package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +4 -4
- package/dist/ledger.js +12 -3
- package/dist/mcp.js +23 -4
- package/dist/merchant-bond-domain.js +64 -0
- package/dist/merchant-bond-exposure.js +78 -0
- package/dist/merchant-bond-watcher.js +26 -0
- package/dist/payment-rails.js +29 -0
- package/dist/platform-receive-accounts.js +94 -0
- package/dist/product-verification.js +93 -0
- package/dist/pwa/acp-feed.js +3 -2
- package/dist/pwa/arbitration-read-admin.js +37 -0
- package/dist/pwa/arbitrator-lifecycle.js +100 -0
- package/dist/pwa/contract-fingerprint.js +18 -0
- package/dist/pwa/direct-pay-guards.js +20 -0
- package/dist/pwa/direct-pay-order-redaction.js +42 -0
- package/dist/pwa/endpoint-actions.js +3 -0
- package/dist/pwa/human-presence.js +2 -6
- package/dist/pwa/public/app-account.js +9 -9
- package/dist/pwa/public/app-admin-disputes.js +55 -0
- package/dist/pwa/public/app-agent-appeal.js +90 -0
- package/dist/pwa/public/app-agent-approvals.js +93 -0
- package/dist/pwa/public/app-agent-pair.js +127 -0
- package/dist/pwa/public/app-ai.js +10 -10
- package/dist/pwa/public/app-arbitrator-admin.js +87 -0
- package/dist/pwa/public/app-arbitrator-entry.js +9 -0
- package/dist/pwa/public/app-bond-deferral-ui.js +9 -0
- package/dist/pwa/public/app-bond-refund-ui.js +66 -0
- package/dist/pwa/public/app-bond-slash-ui.js +74 -0
- package/dist/pwa/public/app-bond-terms-ui.js +23 -0
- package/dist/pwa/public/app-bond-ui.js +108 -0
- package/dist/pwa/public/app-chat-poll.js +29 -0
- package/dist/pwa/public/app-contribution-hub.js +23 -0
- package/dist/pwa/public/app-create-kinds.js +17 -0
- package/dist/pwa/public/app-direct-pay-accounts.js +141 -0
- package/dist/pwa/public/app-direct-pay-buyer.js +72 -0
- package/dist/pwa/public/app-direct-pay-cancel-refund.js +72 -0
- package/dist/pwa/public/app-direct-pay-compliance.js +67 -0
- package/dist/pwa/public/app-direct-pay-copy.js +12 -0
- package/dist/pwa/public/app-direct-pay-deferral-admin.js +72 -0
- package/dist/pwa/public/app-direct-pay-deferral.js +61 -0
- package/dist/pwa/public/app-direct-pay-fee-center.js +33 -0
- package/dist/pwa/public/app-direct-pay-fee-history.js +39 -0
- package/dist/pwa/public/app-direct-pay-fee-ops.js +112 -0
- package/dist/pwa/public/app-direct-pay-fee-request.js +81 -0
- package/dist/pwa/public/app-direct-pay-fee-requests-admin.js +70 -0
- package/dist/pwa/public/app-direct-pay-memo.js +14 -0
- package/dist/pwa/public/app-direct-pay-negotiation.js +35 -0
- package/dist/pwa/public/app-direct-pay-pay.js +15 -0
- package/dist/pwa/public/app-direct-pay-paymodal.js +32 -0
- package/dist/pwa/public/app-direct-pay-product-verify.js +103 -0
- package/dist/pwa/public/app-direct-pay-readiness.js +38 -0
- package/dist/pwa/public/app-direct-pay-reconcile.js +19 -0
- package/dist/pwa/public/app-direct-pay-returns.js +56 -0
- package/dist/pwa/public/app-direct-pay-reveal.js +82 -0
- package/dist/pwa/public/app-direct-pay-sales-report.js +70 -0
- package/dist/pwa/public/app-direct-pay-store-verify.js +100 -0
- package/dist/pwa/public/app-direct-pay.js +226 -0
- package/dist/pwa/public/app-discover.js +20 -20
- package/dist/pwa/public/app-dispute-close-ui.js +38 -0
- package/dist/pwa/public/app-external-links.js +32 -0
- package/dist/pwa/public/app-free-shipping-ui.js +29 -0
- package/dist/pwa/public/app-gmv-rail-split.js +12 -0
- package/dist/pwa/public/app-listing-commerce-ui.js +72 -0
- package/dist/pwa/public/app-listings.js +4 -4
- package/dist/pwa/public/app-mutual-cancel.js +54 -0
- package/dist/pwa/public/app-notif-templates-orders.js +43 -0
- package/dist/pwa/public/app-notif-templates.js +22 -0
- package/dist/pwa/public/app-order-accept-ui.js +158 -0
- package/dist/pwa/public/app-order-errors.js +50 -0
- package/dist/pwa/public/app-order-labels.js +19 -0
- package/dist/pwa/public/app-order-rail-filter.js +26 -0
- package/dist/pwa/public/app-platform-receive-accounts.js +140 -0
- package/dist/pwa/public/app-poll-governor.js +22 -0
- package/dist/pwa/public/app-prelaunch-waz.js +39 -0
- package/dist/pwa/public/app-price.js +55 -0
- package/dist/pwa/public/app-product-media.js +15 -0
- package/dist/pwa/public/app-profile.js +10 -10
- package/dist/pwa/public/app-purchase-terms-ui.js +68 -0
- package/dist/pwa/public/app-sale-regions-ui.js +38 -0
- package/dist/pwa/public/app-seller.js +5 -5
- package/dist/pwa/public/app-shop.js +19 -19
- package/dist/pwa/public/app-trade-tax-ui.js +33 -0
- package/dist/pwa/public/app.js +264 -295
- package/dist/pwa/public/i18n.js +1068 -197
- package/dist/pwa/public/index.html +58 -0
- package/dist/pwa/public/openapi.json +1845 -653
- package/dist/pwa/public/style.css +3 -0
- package/dist/pwa/routes/admin-analytics.js +4 -2
- package/dist/pwa/routes/admin-direct-receive-deposits.js +533 -0
- package/dist/pwa/routes/admin-protocol-params.js +16 -0
- package/dist/pwa/routes/admin-reports.js +31 -5
- package/dist/pwa/routes/agent-governance.js +1 -1
- package/dist/pwa/routes/agent-grants.js +253 -32
- package/dist/pwa/routes/analytics.js +2 -0
- package/dist/pwa/routes/arbitrator.js +67 -14
- package/dist/pwa/routes/bond-seller.js +162 -0
- package/dist/pwa/routes/buyer-feeds.js +2 -1
- package/dist/pwa/routes/chat.js +6 -1
- package/dist/pwa/routes/dashboards.js +2 -1
- package/dist/pwa/routes/direct-pay-availability.js +196 -0
- package/dist/pwa/routes/direct-pay-cancel-refund.js +111 -0
- package/dist/pwa/routes/direct-pay-disclosure-acks.js +79 -0
- package/dist/pwa/routes/direct-pay-pending-accept.js +222 -0
- package/dist/pwa/routes/direct-pay-returns.js +74 -0
- package/dist/pwa/routes/direct-pay-timeouts.js +248 -0
- package/dist/pwa/routes/direct-receive-accounts.js +155 -0
- package/dist/pwa/routes/direct-receive-payment-instructions.js +45 -0
- package/dist/pwa/routes/disputes-read.js +20 -6
- package/dist/pwa/routes/disputes-write.js +91 -33
- package/dist/pwa/routes/external-anchors.js +4 -2
- package/dist/pwa/routes/fee-prepay-requests.js +66 -0
- package/dist/pwa/routes/fx.js +12 -0
- package/dist/pwa/routes/governance-onboarding.js +46 -8
- package/dist/pwa/routes/leaderboard.js +47 -9
- package/dist/pwa/routes/listings.js +2 -2
- package/dist/pwa/routes/logistics.js +6 -2
- package/dist/pwa/routes/manifests.js +38 -0
- package/dist/pwa/routes/me-data.js +5 -2
- package/dist/pwa/routes/mutual-cancel.js +62 -0
- package/dist/pwa/routes/orders-action.js +297 -16
- package/dist/pwa/routes/orders-create.js +21 -6
- package/dist/pwa/routes/orders-read.js +106 -8
- package/dist/pwa/routes/p2p-products.js +2 -2
- package/dist/pwa/routes/platform-receive-accounts.js +111 -0
- package/dist/pwa/routes/products-create.js +15 -4
- package/dist/pwa/routes/products-links.js +34 -0
- package/dist/pwa/routes/products-list.js +2 -1
- package/dist/pwa/routes/products-update.js +37 -3
- package/dist/pwa/routes/profile-identity.js +4 -2
- package/dist/pwa/routes/promoter.js +3 -0
- package/dist/pwa/routes/referral.js +4 -0
- package/dist/pwa/routes/returns.js +60 -4
- package/dist/pwa/routes/rewards-apply.js +10 -5
- package/dist/pwa/routes/rewards-clearing-mature.js +96 -0
- package/dist/pwa/routes/rewards-escrow-expire.js +6 -2
- package/dist/pwa/routes/seller-directpay-report.js +110 -0
- package/dist/pwa/routes/seller-quota.js +5 -1
- package/dist/pwa/routes/shipping-templates.js +219 -0
- package/dist/pwa/routes/shops.js +2 -1
- package/dist/pwa/routes/snf.js +4 -1
- package/dist/pwa/routes/url-claim.js +2 -2
- package/dist/pwa/routes/users-public.js +8 -0
- package/dist/pwa/routes/wallet-read.js +3 -0
- package/dist/pwa/routes/webauthn.js +3 -3
- package/dist/pwa/server.js +112 -132
- package/dist/runtime/agent-grant-scopes.js +69 -1
- package/dist/runtime/webaz-schema-helpers.js +161 -3
- package/dist/sale-regions.js +116 -0
- package/dist/shipping-templates.js +119 -0
- package/dist/store-verification.js +77 -0
- package/dist/trade-tax.js +99 -0
- package/dist/trade-terms.js +76 -0
- package/dist/version.js +1 -1
- package/package.json +128 -2
|
@@ -516,6 +516,9 @@ code, kbd, .mono { word-break: break-all; overflow-wrap: anywhere }
|
|
|
516
516
|
animation: slide-up 0.3s ease-out;
|
|
517
517
|
}
|
|
518
518
|
@keyframes slide-up { from { opacity: 0; transform: translate(-50%, 20px) } to { opacity: 1; transform: translate(-50%, 0) } }
|
|
519
|
+
/* 走查批次2:浮条悬在内容上方会盖住页面底部操作(如争议页上传控件)— 浮条在场时给内容区让位。
|
|
520
|
+
数值构成:banner bottom 70px + banner 自高 ~64px + 余量 16px = 150px(.main 默认 padding-bottom 80px 不够,曾误设 96px 实际只多让 16px) */
|
|
521
|
+
body:has(#install-banner) .main { padding-bottom: 150px }
|
|
519
522
|
.install-banner-icon { font-size: 22px; line-height: 1 }
|
|
520
523
|
.install-banner-body { flex: 1; min-width: 0 }
|
|
521
524
|
.install-banner-title { font-size: 13px; font-weight: 600; line-height: 1.2 }
|
|
@@ -259,10 +259,12 @@ export function registerAdminAnalyticsRoutes(app, deps) {
|
|
|
259
259
|
const nowSec = Math.floor(Date.now() / 1000);
|
|
260
260
|
const escrowByStatus = await dbAll(`SELECT status, COUNT(*) AS n, COALESCE(SUM(amount),0) AS total
|
|
261
261
|
FROM pending_commission_escrow GROUP BY status`);
|
|
262
|
+
// RFC-018: these "pending escrow" stats mean opt-out escrow (matures_at IS NULL); clearing rows
|
|
263
|
+
// (matures_at NOT NULL) don't expire-to-reserve and would mislabel "expiring soon".
|
|
262
264
|
const escrowPendingByPath = await dbAll(`SELECT attribution_path, COUNT(*) AS n, COALESCE(SUM(amount),0) AS total
|
|
263
|
-
FROM pending_commission_escrow WHERE status='pending' GROUP BY attribution_path`);
|
|
265
|
+
FROM pending_commission_escrow WHERE status='pending' AND matures_at IS NULL GROUP BY attribution_path`);
|
|
264
266
|
const expiringSoon = (await dbOne(`SELECT COUNT(*) AS n, COALESCE(SUM(amount),0) AS total
|
|
265
|
-
FROM pending_commission_escrow WHERE status='pending' AND expires_at <= ?`, [nowSec + 86400]));
|
|
267
|
+
FROM pending_commission_escrow WHERE status='pending' AND matures_at IS NULL AND expires_at <= ?`, [nowSec + 86400]));
|
|
266
268
|
// 3. consent 版本:当前 major + 仍停留在旧 major 上的 opted-in 用户数(= auto_downgrade 候选)
|
|
267
269
|
const currentMajor = await dbOne(`SELECT version, effective_at FROM rewards_consent_texts WHERE change_class='major' ORDER BY effective_at DESC LIMIT 1`);
|
|
268
270
|
let staleConsentOptedIn = 0;
|
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
import { confirmProductionReceipt, rejectDeposit, executeBondRefund } from '../../direct-receive-deposits.js';
|
|
2
|
+
import { enumerateBondRefundBlockers } from '../../bond-refund-blockers.js'; // B2:执行前复核(冷静期内可能新增退货等责任)
|
|
3
|
+
import { proposeBondSlash, cancelBondSlashProposal, executeBondSlashProposal, listBondSlashProposals } from '../../bond-slash.js'; // B3:罚没提案/冷静期/执行(人工铁律)
|
|
4
|
+
import { dbAll, dbOne } from '../../layer0-foundation/L0-1-database/db.js'; // B1:保证金申报队列/通知收件人只读(async seam)
|
|
5
|
+
import { reviewAmlFlag } from '../../direct-pay-aml-review.js';
|
|
6
|
+
import { recordKybReview, recordSanctionsScreening, recordAmlFlagIngress, amlDetailHash } from '../../direct-pay-compliance-ingress.js';
|
|
7
|
+
import { readDirectPayLaunchReadiness } from '../../direct-pay-launch-readiness.js';
|
|
8
|
+
import { approveDeferral, rejectDeferral, listDeferrals, satisfyDeferralOnBond } from '../../direct-receive-deferral.js';
|
|
9
|
+
import { listProductVerifications, reviewProductVerification } from '../../product-verification.js';
|
|
10
|
+
import { listStoreVerifications, reviewStoreVerification } from '../../store-verification.js';
|
|
11
|
+
import { requireDirectPayHumanPasskey } from '../direct-pay-guards.js';
|
|
12
|
+
import { recordFeePrepayTopup, recordFeePrepayAdjustment, recordFeePrepayRefund, getDirectPayFeeAccount } from '../../direct-pay-fee-ar.js';
|
|
13
|
+
import { listAllRequests, getRequest as getFeePrepayRequest, approveFeePrepayRequest, rejectFeePrepayRequest } from '../../direct-pay-fee-prepay-request.js';
|
|
14
|
+
// N3:审批结果通知卖家(approve 入账/reject 驳回;此前审批后卖家无任何未读信号)。
|
|
15
|
+
import { createNotification } from '../../layer2-business/L2-6-notifications/notification-engine.js';
|
|
16
|
+
export function registerAdminDirectReceiveDepositsRoutes(app, deps) {
|
|
17
|
+
const { db, requireRootAdmin, consumeGateToken, logAdminAction, getProtocolParam } = deps;
|
|
18
|
+
// GET /api/admin/direct-receive/deposits?status=pending — ROOT 只读:保证金申报队列(核对到账用)。B1。
|
|
19
|
+
app.get('/api/admin/direct-receive/deposits', async (req, res) => {
|
|
20
|
+
const admin = requireRootAdmin(req, res);
|
|
21
|
+
if (!admin)
|
|
22
|
+
return;
|
|
23
|
+
const status = req.query?.status != null ? String(req.query.status) : '';
|
|
24
|
+
const rows = await dbAll(`
|
|
25
|
+
SELECT d.id, d.user_id, d.tier, d.required_amount, d.amount, d.currency, d.deposit_rail, d.status,
|
|
26
|
+
d.external_ref, d.reject_note, d.production_receipt_confirmed_at, d.created_at, d.locked_at,
|
|
27
|
+
u.name AS seller_name, u.handle AS seller_handle
|
|
28
|
+
FROM direct_receive_deposits d JOIN users u ON u.id = d.user_id
|
|
29
|
+
${status ? 'WHERE d.status = ?' : ''} ORDER BY d.created_at DESC LIMIT 200`, status ? [status] : []);
|
|
30
|
+
return void res.json({ deposits: rows });
|
|
31
|
+
});
|
|
32
|
+
// POST /api/admin/direct-receive/deposits/:id/reject — ROOT 驳回申报(未 lock 的申报;不动钱,留说明通知卖家)。B1。
|
|
33
|
+
// 不 Passkey:驳回不授予/不移动任何东西(与缓交 reject 不同 —— 那是资格决定;这里只是"到账核不上"退回重报)。
|
|
34
|
+
app.post('/api/admin/direct-receive/deposits/:id/reject', async (req, res) => {
|
|
35
|
+
const admin = requireRootAdmin(req, res);
|
|
36
|
+
if (!admin)
|
|
37
|
+
return;
|
|
38
|
+
const note = req.body?.note != null ? String(req.body.note).slice(0, 300) : '';
|
|
39
|
+
const r = rejectDeposit(db, { depositId: req.params.id, note });
|
|
40
|
+
logAdminAction(admin.id, 'direct_receive.bond_deposit_reject', 'direct_receive_deposit', req.params.id, { ok: r.ok, note_present: !!note, outcome: r.ok ? r.status : r.reason });
|
|
41
|
+
if (!r.ok)
|
|
42
|
+
return void res.status(409).json({ error: r.reason, error_code: 'BOND_REJECT_FAILED' });
|
|
43
|
+
try {
|
|
44
|
+
const dep = await dbOne('SELECT user_id FROM direct_receive_deposits WHERE id = ?', [req.params.id]);
|
|
45
|
+
if (dep)
|
|
46
|
+
createNotification(db, dep.user_id, null, 'bond_deposit_rejected', '❌ 保证金申报未通过核实', `你的保证金缴纳申报未通过核实${note ? `(${note})` : ''}。请核对付款凭据后重新提交,或联系平台。`, { templateKey: 'bond_deposit_rejected', params: { note: note ? `(${note})` : '' } });
|
|
47
|
+
}
|
|
48
|
+
catch { /* 不阻断 */ }
|
|
49
|
+
return void res.json({ ok: true, status: r.status });
|
|
50
|
+
});
|
|
51
|
+
// ── B3:保证金罚没(人工铁律:仲裁裁定卖家责的直付争议 → 提案 → 冷静期 → ROOT+Passkey 执行;绝不自动)──
|
|
52
|
+
app.get('/api/admin/direct-receive/bond-slash', (req, res) => {
|
|
53
|
+
const admin = requireRootAdmin(req, res);
|
|
54
|
+
if (!admin)
|
|
55
|
+
return;
|
|
56
|
+
const status = req.query?.status != null ? String(req.query.status) : '';
|
|
57
|
+
return void res.json({ proposals: listBondSlashProposals(db, status ? { status } : {}) });
|
|
58
|
+
});
|
|
59
|
+
// 提案(ROOT,审计留痕,不 Passkey —— 提案不动任何东西;执行才是终局动作)。通知卖家(冷静期=申诉窗)。
|
|
60
|
+
app.post('/api/admin/direct-receive/bond-slash/propose', async (req, res) => {
|
|
61
|
+
const admin = requireRootAdmin(req, res);
|
|
62
|
+
if (!admin)
|
|
63
|
+
return;
|
|
64
|
+
const depositId = String(req.body?.deposit_id || '');
|
|
65
|
+
const disputeId = String(req.body?.dispute_id || '');
|
|
66
|
+
if (!depositId || !disputeId)
|
|
67
|
+
return void res.status(400).json({ error: '须提供 deposit_id 与依据 dispute_id', error_code: 'MISSING_BASIS' });
|
|
68
|
+
const coolingDays = Math.max(0, Number(getProtocolParam('direct_pay.bond_slash_cooling_days', 7)) || 7);
|
|
69
|
+
const proposalId = 'bslash_' + Math.random().toString(36).slice(2, 10);
|
|
70
|
+
const r = proposeBondSlash(db, { proposalId, depositId, disputeId, reason: req.body?.reason != null ? String(req.body.reason) : null, proposedBy: admin.id, coolingDays });
|
|
71
|
+
logAdminAction(admin.id, 'direct_receive.bond_slash_propose', 'direct_receive_deposit', depositId, { ok: r.ok, dispute_id: disputeId, outcome: r.ok ? proposalId : r.reason });
|
|
72
|
+
if (!r.ok)
|
|
73
|
+
return void res.status(409).json({ error: r.reason, error_code: 'SLASH_PROPOSE_REJECTED' });
|
|
74
|
+
try {
|
|
75
|
+
const dep = await dbOne('SELECT user_id FROM direct_receive_deposits WHERE id = ?', [depositId]);
|
|
76
|
+
if (dep)
|
|
77
|
+
createNotification(db, dep.user_id, null, 'bond_slash_proposed', '⚠️ 保证金罚没提案(待复核)', `因争议 ${disputeId} 裁定卖家责任,平台已发起保证金罚没提案。冷静期 ${coolingDays} 天内如有异议请联系平台并提供依据;冷静期满后将复核执行(全额罚没,进入处罚金专户,平台不获益)。`, { templateKey: 'bond_slash_proposed', params: { dispute: disputeId, days: coolingDays } });
|
|
78
|
+
}
|
|
79
|
+
catch { /* 不阻断 */ }
|
|
80
|
+
return void res.json({ ok: true, proposal_id: proposalId, cooling_days: coolingDays });
|
|
81
|
+
});
|
|
82
|
+
app.post('/api/admin/direct-receive/bond-slash/:id/cancel', async (req, res) => {
|
|
83
|
+
const admin = requireRootAdmin(req, res);
|
|
84
|
+
if (!admin)
|
|
85
|
+
return;
|
|
86
|
+
const r = cancelBondSlashProposal(db, { proposalId: req.params.id, note: req.body?.note != null ? String(req.body.note) : null });
|
|
87
|
+
logAdminAction(admin.id, 'direct_receive.bond_slash_cancel', 'bond_slash_proposal', req.params.id, { ok: r.ok, outcome: r.ok ? 'cancelled' : r.reason });
|
|
88
|
+
if (!r.ok)
|
|
89
|
+
return void res.status(409).json({ error: r.reason, error_code: 'SLASH_CANCEL_REJECTED' });
|
|
90
|
+
try {
|
|
91
|
+
const p = await dbOne('SELECT seller_id FROM bond_slash_proposals WHERE id = ?', [req.params.id]);
|
|
92
|
+
if (p)
|
|
93
|
+
createNotification(db, p.seller_id, null, 'bond_slash_cancelled', '✅ 保证金罚没提案已撤销', '此前的罚没提案经复核已撤销,你的保证金不受影响。', { templateKey: 'bond_slash_cancelled', params: {} });
|
|
94
|
+
}
|
|
95
|
+
catch { /* 不阻断 */ }
|
|
96
|
+
return void res.json({ ok: true, status: r.status, already: !!r.already });
|
|
97
|
+
});
|
|
98
|
+
// 执行(ROOT + 真人 Passkey,purpose direct_pay_bond_slash 绑 proposal_id;冷静期由域内绝对截止校验)。
|
|
99
|
+
app.post('/api/admin/direct-receive/bond-slash/:id/execute', async (req, res) => {
|
|
100
|
+
const admin = requireRootAdmin(req, res);
|
|
101
|
+
if (!admin)
|
|
102
|
+
return;
|
|
103
|
+
const proposalId = req.params.id;
|
|
104
|
+
const gate = requireDirectPayHumanPasskey({ db, consumeGateToken }, {
|
|
105
|
+
userId: admin.id, webauthnToken: req.body?.webauthn_token, purpose: 'direct_pay_bond_slash',
|
|
106
|
+
validate: (data) => { const d = data; return !!d && d.proposal_id === proposalId; },
|
|
107
|
+
});
|
|
108
|
+
if (!gate.ok) {
|
|
109
|
+
logAdminAction(admin.id, 'direct_receive.bond_slash_execute', 'bond_slash_proposal', proposalId, { ok: false, error_code: gate.error_code });
|
|
110
|
+
return void res.status(403).json({ error: gate.reason, error_code: gate.error_code });
|
|
111
|
+
}
|
|
112
|
+
const txnId = 'bslashtxn_' + Math.random().toString(36).slice(2, 10);
|
|
113
|
+
let r;
|
|
114
|
+
try {
|
|
115
|
+
r = executeBondSlashProposal(db, { proposalId, txnId, nowIso: new Date().toISOString() });
|
|
116
|
+
}
|
|
117
|
+
catch (e) {
|
|
118
|
+
r = { ok: false, reason: e.message };
|
|
119
|
+
}
|
|
120
|
+
logAdminAction(admin.id, 'direct_receive.bond_slash_execute', 'bond_slash_proposal', proposalId, { ok: r.ok, txn_id: txnId, outcome: r.ok ? (r.already ? 'already' : 'executed') : r.reason });
|
|
121
|
+
if (!r.ok)
|
|
122
|
+
return void res.status(409).json({ error: r.reason, error_code: 'SLASH_EXECUTE_REJECTED' });
|
|
123
|
+
if (!r.already) {
|
|
124
|
+
try {
|
|
125
|
+
const p = await dbOne('SELECT seller_id, dispute_id FROM bond_slash_proposals WHERE id = ?', [proposalId]);
|
|
126
|
+
if (p)
|
|
127
|
+
createNotification(db, p.seller_id, null, 'bond_slash_executed', '❌ 保证金已罚没', `依据争议 ${p.dispute_id} 的卖家责任裁定,你的履约保证金已全额罚没(进入处罚金专户,平台不获益),直付资格已吊销。重新缴纳保证金并通过审核后可再次申请开通。`, { templateKey: 'bond_slash_executed', params: { dispute: p.dispute_id } });
|
|
128
|
+
}
|
|
129
|
+
catch { /* 不阻断 */ }
|
|
130
|
+
}
|
|
131
|
+
return void res.json({ ok: true, status: r.status, already: !!r.already });
|
|
132
|
+
});
|
|
133
|
+
// POST /api/admin/direct-receive/deposits/:id/execute-refund — ROOT + 真人 Passkey:记录已完成的【场外】保证金退还(B2)。
|
|
134
|
+
// 前置:refunding + 冷静期满(param direct_pay.bond_refund_cooling_days,默认 14d,域内校验)+ 【执行时复核】
|
|
135
|
+
// unlock blockers(冷静期内可能新增退货/欠费等责任 —— 有任一即拒)。凭据必填;真实退款发生在协议外,此处只记录。
|
|
136
|
+
app.post('/api/admin/direct-receive/deposits/:id/execute-refund', async (req, res) => {
|
|
137
|
+
const admin = requireRootAdmin(req, res);
|
|
138
|
+
if (!admin)
|
|
139
|
+
return;
|
|
140
|
+
const depositId = req.params.id;
|
|
141
|
+
const evidenceRef = String(req.body?.evidence_ref || '').trim();
|
|
142
|
+
const gate = requireDirectPayHumanPasskey({ db, consumeGateToken }, {
|
|
143
|
+
userId: admin.id, webauthnToken: req.body?.webauthn_token, purpose: 'direct_receive_bond_refund',
|
|
144
|
+
validate: (data) => { const d = data; return !!d && d.deposit_id === depositId && d.evidence_ref === evidenceRef; },
|
|
145
|
+
});
|
|
146
|
+
if (!gate.ok) {
|
|
147
|
+
logAdminAction(admin.id, 'direct_receive.bond_refund_execute', 'direct_receive_deposit', depositId, { ok: false, error_code: gate.error_code });
|
|
148
|
+
return void res.status(403).json({ error: gate.reason, error_code: gate.error_code });
|
|
149
|
+
}
|
|
150
|
+
const dep = await dbOne('SELECT user_id FROM direct_receive_deposits WHERE id = ?', [depositId]);
|
|
151
|
+
if (!dep)
|
|
152
|
+
return void res.status(404).json({ error: '存款不存在', error_code: 'DEPOSIT_NOT_FOUND' });
|
|
153
|
+
const blockers = enumerateBondRefundBlockers(db, dep.user_id);
|
|
154
|
+
if (blockers.length > 0) {
|
|
155
|
+
logAdminAction(admin.id, 'direct_receive.bond_refund_execute', 'direct_receive_deposit', depositId, { ok: false, outcome: 'blocked', blockers: blockers.map(b => b.code) });
|
|
156
|
+
return void res.status(409).json({ error: '卖家仍有未了结直付责任,不可退还', error_code: 'REFUND_BLOCKED', blockers });
|
|
157
|
+
}
|
|
158
|
+
const coolingDays = Math.max(0, Number(getProtocolParam('direct_pay.bond_refund_cooling_days', 14)) || 14);
|
|
159
|
+
const r = executeBondRefund(db, { depositId, nowIso: new Date().toISOString(), coolingDays, evidenceRef });
|
|
160
|
+
logAdminAction(admin.id, 'direct_receive.bond_refund_execute', 'direct_receive_deposit', depositId, { ok: r.ok, evidence_present: !!evidenceRef, outcome: r.ok ? (r.already ? 'already' : 'refunded') : r.reason });
|
|
161
|
+
if (!r.ok)
|
|
162
|
+
return void res.status(409).json({ error: r.reason, error_code: 'REFUND_EXECUTE_REJECTED' });
|
|
163
|
+
if (!r.already) {
|
|
164
|
+
try {
|
|
165
|
+
createNotification(db, dep.user_id, null, 'bond_refund_executed', '✅ 履约保证金已退还', `你的保证金已在协议外退还并记录(凭据:${evidenceRef.slice(0, 60)})。直付资格随保证金退出关闭;重新缴纳后可再次开通。`, { templateKey: 'bond_refund_executed', params: { evidence: evidenceRef.slice(0, 60) } });
|
|
166
|
+
}
|
|
167
|
+
catch { /* 不阻断 */ }
|
|
168
|
+
}
|
|
169
|
+
return void res.json({ ok: true, status: r.status, already: !!r.already });
|
|
170
|
+
});
|
|
171
|
+
// POST /api/admin/direct-receive/deposits/:id/confirm-production — ROOT + 真人 Passkey 手动确认生产保证金 receipt。
|
|
172
|
+
// 当前恒 fail-closed(无 legal-cleared rail → assert 抛 → PRODUCTION_RAIL_NOT_CLEARED)。
|
|
173
|
+
app.post('/api/admin/direct-receive/deposits/:id/confirm-production', async (req, res) => {
|
|
174
|
+
const admin = requireRootAdmin(req, res);
|
|
175
|
+
if (!admin)
|
|
176
|
+
return;
|
|
177
|
+
const depositId = req.params.id;
|
|
178
|
+
const railId = String(req.body?.rail_id || '');
|
|
179
|
+
const amountUnits = Number(req.body?.expected_amount_units);
|
|
180
|
+
const receiptRef = String(req.body?.receipt_ref || '');
|
|
181
|
+
// jurisdiction=【平台收款主体法域】(P2 澄清:非卖家法域;卖家资格由 KYB/制裁/AML 独立把守)。
|
|
182
|
+
// 自由输入但被域内 DIRECT_PAY_BOND_JURISDICTIONS 严格白名单硬约束(非白名单值必拒),并进 Passkey purpose_data + 审计。
|
|
183
|
+
const jurisdiction = String(req.body?.jurisdiction || '');
|
|
184
|
+
const webauthnToken = req.body?.webauthn_token;
|
|
185
|
+
// 现场真人 Passkey(铁律)。purpose_data 绑定【完整动作字段】:deposit/rail/amount/receipt/jurisdiction 全等才放行,
|
|
186
|
+
// 防止把一次签名复用到另一笔/另一参数的确认上。
|
|
187
|
+
const gate = requireDirectPayHumanPasskey({ db, consumeGateToken }, {
|
|
188
|
+
userId: admin.id, webauthnToken, purpose: 'direct_receive_production_confirm',
|
|
189
|
+
validate: (data) => {
|
|
190
|
+
const d = data;
|
|
191
|
+
return !!d && d.deposit_id === depositId && d.rail_id === railId && Number(d.amount_units) === amountUnits
|
|
192
|
+
&& d.receipt_ref === receiptRef && d.jurisdiction === jurisdiction;
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
// 每次 ROOT 尝试都审计 —— 含【gate 失败】(生产保证金确认本身是敏感控制事件,缺 Passkey / 缺 token /
|
|
196
|
+
// purpose_data 不符的 ROOT 尝试同样要留痕)。非 ROOT 不审计(requireRootAdmin 已拦,无可信 admin 身份)。
|
|
197
|
+
if (!gate.ok) {
|
|
198
|
+
logAdminAction(admin.id, 'direct_receive.production_confirm', 'direct_receive_deposit', depositId, { rail_id: railId, jurisdiction, ok: false, outcome: gate.error_code === 'PASSKEY_REQUIRED_FOR_DIRECT_PAY' ? 'passkey_required' : 'human_presence_required', error_code: gate.error_code });
|
|
199
|
+
return void res.status(403).json({ error: gate.reason, error_code: gate.error_code });
|
|
200
|
+
}
|
|
201
|
+
// 写入只走唯一 writer。assert 抛 → 当前恒 fail-closed。每次 ROOT 尝试都审计(含结果)。
|
|
202
|
+
try {
|
|
203
|
+
const r = confirmProductionReceipt(db, { depositId, railId, expectedAmountUnits: amountUnits, receiptRef, jurisdiction });
|
|
204
|
+
logAdminAction(admin.id, 'direct_receive.production_confirm', 'direct_receive_deposit', depositId, { rail_id: railId, jurisdiction, ok: r.ok, outcome: r.ok ? (r.already ? 'already' : 'confirmed') : r.reason });
|
|
205
|
+
if (!r.ok)
|
|
206
|
+
return void res.status(409).json({ error: r.reason, error_code: 'PRODUCTION_CONFIRM_REJECTED' });
|
|
207
|
+
// B1:确认成功 → 通知卖家保证金已生效;B4:缓交期间缴清 → granted 缓交转 satisfied(解除额度压低)+ 告知。best-effort 不阻断。
|
|
208
|
+
if (!r.already) {
|
|
209
|
+
try {
|
|
210
|
+
const dep = await dbOne('SELECT user_id FROM direct_receive_deposits WHERE id = ?', [depositId]);
|
|
211
|
+
if (dep) {
|
|
212
|
+
const converted = satisfyDeferralOnBond(db, dep.user_id);
|
|
213
|
+
createNotification(db, dep.user_id, null, 'bond_deposit_confirmed', '✅ 履约保证金已确认锁定', `你的保证金已核实到账并正式锁定,直付入场的保证金门已满足。${converted > 0 ? '缓交资格已转正式,额度限制同步解除。' : ''}退出时可申请退还(须无未了结直付责任)。`, { templateKey: 'bond_deposit_confirmed', params: { converted } });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
catch { /* 通知失败不阻断 */ }
|
|
217
|
+
}
|
|
218
|
+
return void res.json({ ok: true, status: r.status, already: !!r.already });
|
|
219
|
+
}
|
|
220
|
+
catch (e) {
|
|
221
|
+
// assertProductionDepositRail(及任何写前异常)→ 生产 rail 未 legal-clear → fail-closed。
|
|
222
|
+
logAdminAction(admin.id, 'direct_receive.production_confirm', 'direct_receive_deposit', depositId, { rail_id: railId, jurisdiction, ok: false, outcome: 'rail_not_cleared', error: e.message });
|
|
223
|
+
return void res.status(409).json({ error: '生产保证金收款轨未通过法务/生产放行,暂不可确认', error_code: 'PRODUCTION_RAIL_NOT_CLEARED' });
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
// POST /api/admin/direct-receive/aml-flags/:id/review — ROOT + 真人 Passkey 复核单条 AML flag (PR-6E)。
|
|
227
|
+
// route 只做 auth + gate + 参数校验 + 调 reviewAmlFlag(唯一 review writer,原子改 flag + 写 audit)。
|
|
228
|
+
app.post('/api/admin/direct-receive/aml-flags/:id/review', (req, res) => {
|
|
229
|
+
const admin = requireRootAdmin(req, res);
|
|
230
|
+
if (!admin)
|
|
231
|
+
return;
|
|
232
|
+
const flagId = req.params.id;
|
|
233
|
+
const decision = String(req.body?.decision || '');
|
|
234
|
+
const notes = req.body?.notes != null ? String(req.body.notes) : undefined;
|
|
235
|
+
const webauthnToken = req.body?.webauthn_token;
|
|
236
|
+
// 现场真人 Passkey(铁律)。purpose_data 绑 flag_id + decision:一次签名只用于该 flag 的该决策,杜绝复用。
|
|
237
|
+
const gate = requireDirectPayHumanPasskey({ db, consumeGateToken }, {
|
|
238
|
+
userId: admin.id, webauthnToken, purpose: 'direct_pay_aml_review',
|
|
239
|
+
validate: (data) => {
|
|
240
|
+
const d = data;
|
|
241
|
+
return !!d && d.flag_id === flagId && d.decision === decision;
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
if (!gate.ok) {
|
|
245
|
+
// gate 失败也审计(无 PII:flag_id + decision + 结果);成功复核的审计由 reviewAmlFlag 原子写入。
|
|
246
|
+
logAdminAction(admin.id, 'direct_pay.aml_review', 'aml_flag', flagId, { decision, ok: false, outcome: gate.error_code === 'PASSKEY_REQUIRED_FOR_DIRECT_PAY' ? 'passkey_required' : 'human_presence_required', error_code: gate.error_code });
|
|
247
|
+
return void res.status(403).json({ error: gate.reason, error_code: gate.error_code });
|
|
248
|
+
}
|
|
249
|
+
const r = reviewAmlFlag(db, { flagId, reviewerId: admin.id, decision, notes });
|
|
250
|
+
if (!r.ok) {
|
|
251
|
+
const status = r.error === 'FLAG_NOT_FOUND' ? 404 : 400;
|
|
252
|
+
return void res.status(status).json({ error: 'AML flag 复核失败', error_code: r.error });
|
|
253
|
+
}
|
|
254
|
+
return void res.json({ ok: true, flag_id: r.flagId, decision: r.decision, status: r.status, disposition: r.disposition });
|
|
255
|
+
});
|
|
256
|
+
// ── PR-6F 合规 ingress(受控写入入口)。三端点共用:ROOT + 真人 Passkey(purpose-bound)+ 调 ingress helper。 ──
|
|
257
|
+
// route 只做 auth + gate + 取参 + 调 helper(helper 原子写台账 + 审计);gate 失败也审计。无 vendor 外呼、无资金/状态机改动。
|
|
258
|
+
const str = (v) => v != null ? String(v) : '';
|
|
259
|
+
const opt = (v) => v != null ? String(v) : undefined;
|
|
260
|
+
// gatedIngress 返回一个 express handler(literal-path app.post 调用之 —— 让 API-doc 扫描器能识别端点)。
|
|
261
|
+
const gatedIngress = (purpose, makeValidate, run, failAudit) => (req, res) => {
|
|
262
|
+
const admin = requireRootAdmin(req, res);
|
|
263
|
+
if (!admin)
|
|
264
|
+
return;
|
|
265
|
+
const gate = requireDirectPayHumanPasskey({ db, consumeGateToken }, {
|
|
266
|
+
userId: admin.id, webauthnToken: req.body?.webauthn_token, purpose, validate: makeValidate(req),
|
|
267
|
+
});
|
|
268
|
+
if (!gate.ok) {
|
|
269
|
+
const fa = failAudit(req);
|
|
270
|
+
logAdminAction(admin.id, purpose, 'user', fa.targetId, { ...fa.detail, ok: false, error_code: gate.error_code });
|
|
271
|
+
return void res.status(403).json({ error: gate.reason, error_code: gate.error_code });
|
|
272
|
+
}
|
|
273
|
+
const r = run(req, admin.id);
|
|
274
|
+
if (!r.ok) {
|
|
275
|
+
// 业务校验失败(Passkey 已过)也留痕:money-adjacent admin 尝试(如退款余额不足被拒)必须可审计。
|
|
276
|
+
const fa = failAudit(req);
|
|
277
|
+
logAdminAction(admin.id, purpose, 'user', fa.targetId, { ...fa.detail, ok: false, error_code: r.error });
|
|
278
|
+
return void res.status(400).json({ error: '合规 ingress 失败', error_code: r.error });
|
|
279
|
+
}
|
|
280
|
+
return void res.json({ ok: true, id: r.id });
|
|
281
|
+
};
|
|
282
|
+
// POST /api/admin/direct-receive/kyb-reviews — KYB 复核结论 ingress(ROOT + 真人 Passkey)。
|
|
283
|
+
// Passkey purpose_data 绑定【完整写入内容】(user_id+status+provider_ref+expires_at):签 A 写 B 一律拒。
|
|
284
|
+
app.post('/api/admin/direct-receive/kyb-reviews', gatedIngress('direct_pay_kyb_ingress', (req) => (data) => {
|
|
285
|
+
const d = data;
|
|
286
|
+
return !!d && str(d.user_id) === str(req.body?.user_id) && str(d.status) === str(req.body?.status)
|
|
287
|
+
&& str(d.provider_ref) === str(req.body?.provider_ref) && str(d.expires_at) === str(req.body?.expires_at);
|
|
288
|
+
}, (req, adminId) => recordKybReview(db, { userId: str(req.body?.user_id), reviewerId: adminId, status: str(req.body?.status), providerRef: opt(req.body?.provider_ref), expiresAt: opt(req.body?.expires_at) }), (req) => ({ targetId: str(req.body?.user_id), detail: { kyb_status: str(req.body?.status) } })));
|
|
289
|
+
// POST /api/admin/direct-receive/sanctions-screenings — 制裁筛查结论 ingress(ROOT + 真人 Passkey;high-risk)。
|
|
290
|
+
// purpose_data 绑定 user_id+status+provider_ref+expires_at。
|
|
291
|
+
app.post('/api/admin/direct-receive/sanctions-screenings', gatedIngress('direct_pay_sanctions_ingress', (req) => (data) => {
|
|
292
|
+
const d = data;
|
|
293
|
+
return !!d && str(d.user_id) === str(req.body?.user_id) && str(d.status) === str(req.body?.status)
|
|
294
|
+
&& str(d.provider_ref) === str(req.body?.provider_ref) && str(d.expires_at) === str(req.body?.expires_at);
|
|
295
|
+
}, (req, adminId) => recordSanctionsScreening(db, { userId: str(req.body?.user_id), reviewerId: adminId, status: str(req.body?.status), providerRef: opt(req.body?.provider_ref), expiresAt: opt(req.body?.expires_at) }), (req) => ({ targetId: str(req.body?.user_id), detail: { sanctions_status: str(req.body?.status) } })));
|
|
296
|
+
// POST /api/admin/direct-receive/aml-flags — AML flag ingress(ROOT + 真人 Passkey;high-risk)。与 .../aml-flags/:id/review 区分。
|
|
297
|
+
// purpose_data 绑定 user_id+rule+severity+status+related_order_id+detail_hash(canonical):签 A 写 B 一律拒。
|
|
298
|
+
const amlDetail = (req) => (req.body?.detail && typeof req.body.detail === 'object' && !Array.isArray(req.body.detail)) ? req.body.detail : undefined;
|
|
299
|
+
app.post('/api/admin/direct-receive/aml-flags', gatedIngress('direct_pay_aml_ingress', (req) => (data) => {
|
|
300
|
+
const d = data;
|
|
301
|
+
return !!d && str(d.user_id) === str(req.body?.user_id) && str(d.rule) === str(req.body?.rule) && str(d.severity) === str(req.body?.severity)
|
|
302
|
+
&& str(d.status) === str(req.body?.status) && str(d.related_order_id) === str(req.body?.related_order_id) && str(d.detail_hash) === amlDetailHash(amlDetail(req));
|
|
303
|
+
}, (req, adminId) => recordAmlFlagIngress(db, { userId: str(req.body?.user_id), reviewerId: adminId, rule: str(req.body?.rule), severity: str(req.body?.severity), status: str(req.body?.status), relatedOrderId: opt(req.body?.related_order_id), detail: amlDetail(req) }), (req) => ({ targetId: str(req.body?.user_id), detail: { rule: str(req.body?.rule), severity: str(req.body?.severity), aml_status: str(req.body?.status) } })));
|
|
304
|
+
// POST /api/admin/direct-receive/fee-prepay — 记录商家【平台服务费预付款】(ROOT + 真人 Passkey)。append-only;
|
|
305
|
+
// invoice_id NULL = 未分配预充值 → 计入 available_prepay(首单后续建单门)。purpose_data 绑定 seller_id+amount_units+method+evidence_ref。
|
|
306
|
+
// 不碰 buyer wallet/escrow/order/settlement/refund;非买家 escrow/保证金/penalty。本轮无"余额退款"(仅正向预付款登记)。
|
|
307
|
+
app.post('/api/admin/direct-receive/fee-prepay', gatedIngress('direct_pay_fee_prepay_record', (req) => (data) => {
|
|
308
|
+
const d = data;
|
|
309
|
+
return !!d && str(d.seller_id) === str(req.body?.seller_id) && Number(d.amount_units) === Number(req.body?.amount_units)
|
|
310
|
+
&& str(d.method) === str(req.body?.method) && str(d.evidence_ref) === str(req.body?.evidence_ref);
|
|
311
|
+
}, (req, adminId) => recordFeePrepayTopup(db, { sellerId: str(req.body?.seller_id), amountUnits: Number(req.body?.amount_units), method: str(req.body?.method), recordedBy: adminId, evidenceRef: opt(req.body?.evidence_ref), note: opt(req.body?.note) }), (req) => ({ targetId: str(req.body?.seller_id), detail: { amount_units: Number(req.body?.amount_units), method: str(req.body?.method) } })));
|
|
312
|
+
// POST /api/admin/direct-receive/fee-adjust — 平台服务费账务【更正】(ROOT + 真人 Passkey)。带符号 delta;append-only + audit。
|
|
313
|
+
// ≠ 退款(不动真钱,只调记账)。purpose_data 绑 seller_id+delta_units+reason。
|
|
314
|
+
app.post('/api/admin/direct-receive/fee-adjust', gatedIngress('direct_pay_fee_adjust', (req) => (data) => {
|
|
315
|
+
const d = data;
|
|
316
|
+
return !!d && str(d.seller_id) === str(req.body?.seller_id) && Number(d.delta_units) === Number(req.body?.delta_units) && str(d.reason) === str(req.body?.reason);
|
|
317
|
+
}, (req, adminId) => recordFeePrepayAdjustment(db, { sellerId: str(req.body?.seller_id), deltaUnits: Number(req.body?.delta_units), reason: str(req.body?.reason), recordedBy: adminId }), (req) => ({ targetId: str(req.body?.seller_id), detail: { delta_units: Number(req.body?.delta_units) } })));
|
|
318
|
+
// POST /api/admin/direct-receive/fee-refund — 平台服务费余额【退款】(ROOT + 真人 Passkey)。真实退还未消耗预付款;
|
|
319
|
+
// amount ≤ 当前 available(helper 同事务校验)。append-only + audit。purpose_data 绑 seller_id+amount_units+method+evidence_ref。
|
|
320
|
+
app.post('/api/admin/direct-receive/fee-refund', gatedIngress('direct_pay_fee_refund', (req) => (data) => {
|
|
321
|
+
const d = data;
|
|
322
|
+
return !!d && str(d.seller_id) === str(req.body?.seller_id) && Number(d.amount_units) === Number(req.body?.amount_units)
|
|
323
|
+
&& str(d.method) === str(req.body?.method) && str(d.evidence_ref) === str(req.body?.evidence_ref);
|
|
324
|
+
}, (req, adminId) => recordFeePrepayRefund(db, { sellerId: str(req.body?.seller_id), amountUnits: Number(req.body?.amount_units), method: str(req.body?.method), recordedBy: adminId, evidenceRef: opt(req.body?.evidence_ref), reason: opt(req.body?.reason) }), (req) => ({ targetId: str(req.body?.seller_id), detail: { amount_units: Number(req.body?.amount_units), method: str(req.body?.method) } })));
|
|
325
|
+
// GET /api/admin/direct-receive/fee-account/:seller_id — ROOT 只读:某商家平台服务费账户汇总(余额/预充值/应收/调整/退款/在途预估/宽限)。
|
|
326
|
+
// 只读诊断,不写、无 Passkey(读不授权能力);卖家私密财务,买家/卖家拿不到此 admin 视图。
|
|
327
|
+
app.get('/api/admin/direct-receive/fee-account/:seller_id', (req, res) => {
|
|
328
|
+
const admin = requireRootAdmin(req, res);
|
|
329
|
+
if (!admin)
|
|
330
|
+
return;
|
|
331
|
+
return void res.json({ ok: true, account: getDirectPayFeeAccount(db, str(req.params.seller_id)) });
|
|
332
|
+
});
|
|
333
|
+
// GET /api/admin/direct-receive/fee-prepay-requests?status=pending — ROOT 只读:预充值申请队列(核对到账用)。
|
|
334
|
+
app.get('/api/admin/direct-receive/fee-prepay-requests', (req, res) => {
|
|
335
|
+
const admin = requireRootAdmin(req, res);
|
|
336
|
+
if (!admin)
|
|
337
|
+
return;
|
|
338
|
+
return void res.json({ ok: true, requests: listAllRequests(db, req.query.status ? str(req.query.status) : undefined) });
|
|
339
|
+
});
|
|
340
|
+
// POST /api/admin/direct-receive/fee-prepay-requests/:id/approve — ROOT + Passkey【确认真实到账 → 入账】(唯一动钱)。
|
|
341
|
+
// ⚠️ 独立 purpose 'direct_pay_fee_prepay_request_approve'(≠ 手动入账的 'direct_pay_fee_prepay_record')——
|
|
342
|
+
// 否则"批准申请"的 token 能被拿去打手动 /fee-prepay 记一笔【未关联 fpr】的预充值,原申请仍 pending → 双入账/断链。
|
|
343
|
+
// purpose_data 绑 request_id + seller_id + amount_units + method(把入账金额/对象钉进 Passkey)。原子:approved + recordFeePrepay + 关联 payment。
|
|
344
|
+
app.post('/api/admin/direct-receive/fee-prepay-requests/:id/approve', gatedIngress('direct_pay_fee_prepay_request_approve', (req) => (data) => {
|
|
345
|
+
const d = data;
|
|
346
|
+
const fr = getFeePrepayRequest(db, str(req.params.id));
|
|
347
|
+
return !!d && !!fr && d.request_id === str(req.params.id) && d.seller_id === fr.seller_id && Number(d.amount_units) === Number(fr.amount_units) && str(d.method) === str(req.body?.method);
|
|
348
|
+
}, (req, adminId) => {
|
|
349
|
+
const fr = getFeePrepayRequest(db, str(req.params.id));
|
|
350
|
+
const r = approveFeePrepayRequest(db, { requestId: str(req.params.id), adminId, method: str(req.body?.method), reviewNote: opt(req.body?.note) });
|
|
351
|
+
if (r.ok && fr) {
|
|
352
|
+
const amt = Number(fr.amount_units) / 1_000_000;
|
|
353
|
+
try {
|
|
354
|
+
createNotification(db, fr.seller_id, null, 'dp_fee_prepay_approved', '✅ 预充值已确认入账', `你的平台服务费预充值 ${amt} USDC 已确认入账,直付新单额度已恢复。`, { templateKey: 'dp_fee_prepay_approved', params: { amount: amt } });
|
|
355
|
+
}
|
|
356
|
+
catch { /* 通知失败不阻断入账 */ }
|
|
357
|
+
}
|
|
358
|
+
return r.ok ? { ok: true, id: r.paymentId } : { ok: false, error: r.error };
|
|
359
|
+
}, (req) => { const fr = getFeePrepayRequest(db, str(req.params.id)); return { targetId: fr?.seller_id ?? str(req.params.id), detail: { request_id: str(req.params.id), amount_units: fr?.amount_units, method: str(req.body?.method) } }; }));
|
|
360
|
+
// POST /api/admin/direct-receive/fee-prepay-requests/:id/reject — ROOT + Passkey(不动钱)。purpose_data 绑 request_id。
|
|
361
|
+
app.post('/api/admin/direct-receive/fee-prepay-requests/:id/reject', gatedIngress('direct_pay_fee_prepay_reject', (req) => (data) => { const d = data; return !!d && d.request_id === str(req.params.id); }, (req, adminId) => {
|
|
362
|
+
const fr = getFeePrepayRequest(db, str(req.params.id));
|
|
363
|
+
const r = rejectFeePrepayRequest(db, { requestId: str(req.params.id), adminId, reviewNote: opt(req.body?.note) });
|
|
364
|
+
if (r.ok && fr) {
|
|
365
|
+
const note = opt(req.body?.note) ? `(${opt(req.body?.note)})` : '';
|
|
366
|
+
try {
|
|
367
|
+
createNotification(db, fr.seller_id, null, 'dp_fee_prepay_rejected', '❌ 预充值申请未通过', `你的平台服务费预充值申请未通过${note}。请核对付款凭据后重新提交,或联系平台。`, { templateKey: 'dp_fee_prepay_rejected', params: { note } });
|
|
368
|
+
}
|
|
369
|
+
catch { /* 通知失败不阻断 */ }
|
|
370
|
+
}
|
|
371
|
+
return r;
|
|
372
|
+
}, (req) => ({ targetId: str(req.params.id), detail: { request_id: str(req.params.id) } })));
|
|
373
|
+
// POST /api/admin/direct-receive/readiness — ROOT + 真人 Passkey:返回【完整】Direct Pay launch readiness(blockers + facts,
|
|
374
|
+
// 含 KYB/sanctions/AML/base-bond/rail clearance 全细节)。只读诊断(不写库、不 flip launch);ROOT 专用,买家/卖家拿不到。
|
|
375
|
+
app.post('/api/admin/direct-receive/readiness', (req, res) => {
|
|
376
|
+
const admin = requireRootAdmin(req, res);
|
|
377
|
+
if (!admin)
|
|
378
|
+
return;
|
|
379
|
+
const sellerId = req.body?.seller_id != null ? String(req.body.seller_id) : undefined;
|
|
380
|
+
const gate = requireDirectPayHumanPasskey({ db, consumeGateToken }, {
|
|
381
|
+
userId: admin.id, webauthnToken: req.body?.webauthn_token, purpose: 'direct_pay_admin_readiness',
|
|
382
|
+
validate: (data) => { const d = data; return !!d && String(d.seller_id ?? '') === String(sellerId ?? ''); },
|
|
383
|
+
});
|
|
384
|
+
if (!gate.ok) {
|
|
385
|
+
logAdminAction(admin.id, 'direct_pay.admin_readiness', 'user', sellerId ?? null, { ok: false, error_code: gate.error_code });
|
|
386
|
+
return void res.status(403).json({ error: gate.reason, error_code: gate.error_code });
|
|
387
|
+
}
|
|
388
|
+
const readiness = readDirectPayLaunchReadiness(db, { getProtocolParam, sellerId });
|
|
389
|
+
logAdminAction(admin.id, 'direct_pay.admin_readiness', 'user', sellerId ?? null, { ok: true, ready: readiness.ready, blocker_count: readiness.blockers.length });
|
|
390
|
+
return void res.json(readiness);
|
|
391
|
+
});
|
|
392
|
+
// ── 缓交(deferred base-bond)审批队列。读 = ROOT;批准/拒绝 = ROOT + 真人 Passkey(铁律,授予绝不自动)。 ──
|
|
393
|
+
// approveDeferral/rejectDeferral 是唯一 writer;本文件零 direct_receive_deferrals 直写。审批只改缓交状态机,
|
|
394
|
+
// 【不】碰 buyer wallet/escrow/order/settlement/refund/privileges,也不 flip launch。
|
|
395
|
+
const DEFERRAL_STATUSES = new Set(['pending', 'granted', 'rejected', 'expired']);
|
|
396
|
+
// GET /api/admin/direct-receive/deferrals?status=pending — ROOT 审批队列(默认全部;可按 status 过滤)。纯读。
|
|
397
|
+
app.get('/api/admin/direct-receive/deferrals', (req, res) => {
|
|
398
|
+
const admin = requireRootAdmin(req, res);
|
|
399
|
+
if (!admin)
|
|
400
|
+
return;
|
|
401
|
+
const status = req.query?.status != null ? String(req.query.status) : '';
|
|
402
|
+
if (status && !DEFERRAL_STATUSES.has(status))
|
|
403
|
+
return void res.status(400).json({ error: '非法 status', error_code: 'BAD_STATUS' });
|
|
404
|
+
return void res.json({ deferrals: listDeferrals(db, status ? { status: status } : {}) });
|
|
405
|
+
});
|
|
406
|
+
// POST /api/admin/direct-receive/deferrals/:id/approve — ROOT + 真人 Passkey 批准缓交。
|
|
407
|
+
// Passkey purpose_data 绑定【完整审批条款】(deferral_id + reduced_quota_factor + grace_days):签 A 批 B / 改条款一律拒。
|
|
408
|
+
app.post('/api/admin/direct-receive/deferrals/:id/approve', (req, res) => {
|
|
409
|
+
const admin = requireRootAdmin(req, res);
|
|
410
|
+
if (!admin)
|
|
411
|
+
return;
|
|
412
|
+
const deferralId = req.params.id;
|
|
413
|
+
const rqfRaw = req.body?.reduced_quota_factor;
|
|
414
|
+
const graceRaw = req.body?.grace_days;
|
|
415
|
+
const webauthnToken = req.body?.webauthn_token;
|
|
416
|
+
const gate = requireDirectPayHumanPasskey({ db, consumeGateToken }, {
|
|
417
|
+
userId: admin.id, webauthnToken, purpose: 'direct_pay_deferral_approve',
|
|
418
|
+
validate: (data) => {
|
|
419
|
+
const d = data;
|
|
420
|
+
return !!d && str(d.deferral_id) === deferralId && str(d.reduced_quota_factor) === str(rqfRaw) && str(d.grace_days) === str(graceRaw);
|
|
421
|
+
},
|
|
422
|
+
});
|
|
423
|
+
if (!gate.ok) {
|
|
424
|
+
logAdminAction(admin.id, 'direct_pay.deferral_approve', 'direct_receive_deferral', deferralId, { ok: false, outcome: gate.error_code === 'PASSKEY_REQUIRED_FOR_DIRECT_PAY' ? 'passkey_required' : 'human_presence_required', error_code: gate.error_code });
|
|
425
|
+
return void res.status(403).json({ error: gate.reason, error_code: gate.error_code });
|
|
426
|
+
}
|
|
427
|
+
const r = approveDeferral(db, { deferralId, adminId: admin.id, nowIso: new Date().toISOString(),
|
|
428
|
+
graceDays: graceRaw != null ? Number(graceRaw) : undefined, reducedQuotaFactor: rqfRaw != null ? Number(rqfRaw) : undefined });
|
|
429
|
+
logAdminAction(admin.id, 'direct_pay.deferral_approve', 'direct_receive_deferral', deferralId, { ok: r.ok, outcome: r.ok ? (r.already ? 'already' : 'granted') : r.reason });
|
|
430
|
+
if (!r.ok)
|
|
431
|
+
return void res.status(409).json({ error: r.reason, error_code: 'DEFERRAL_APPROVE_REJECTED' });
|
|
432
|
+
return void res.json({ ok: true, status: r.status, already: !!r.already });
|
|
433
|
+
});
|
|
434
|
+
// POST /api/admin/direct-receive/deferrals/:id/reject — ROOT + 真人 Passkey 拒绝缓交。purpose_data 绑 deferral_id。
|
|
435
|
+
app.post('/api/admin/direct-receive/deferrals/:id/reject', (req, res) => {
|
|
436
|
+
const admin = requireRootAdmin(req, res);
|
|
437
|
+
if (!admin)
|
|
438
|
+
return;
|
|
439
|
+
const deferralId = req.params.id;
|
|
440
|
+
const webauthnToken = req.body?.webauthn_token;
|
|
441
|
+
const gate = requireDirectPayHumanPasskey({ db, consumeGateToken }, {
|
|
442
|
+
userId: admin.id, webauthnToken, purpose: 'direct_pay_deferral_reject',
|
|
443
|
+
validate: (data) => { const d = data; return !!d && str(d.deferral_id) === deferralId; },
|
|
444
|
+
});
|
|
445
|
+
if (!gate.ok) {
|
|
446
|
+
logAdminAction(admin.id, 'direct_pay.deferral_reject', 'direct_receive_deferral', deferralId, { ok: false, outcome: gate.error_code === 'PASSKEY_REQUIRED_FOR_DIRECT_PAY' ? 'passkey_required' : 'human_presence_required', error_code: gate.error_code });
|
|
447
|
+
return void res.status(403).json({ error: gate.reason, error_code: gate.error_code });
|
|
448
|
+
}
|
|
449
|
+
const r = rejectDeferral(db, { deferralId, adminId: admin.id });
|
|
450
|
+
logAdminAction(admin.id, 'direct_pay.deferral_reject', 'direct_receive_deferral', deferralId, { ok: r.ok, outcome: r.ok ? (r.already ? 'already' : 'rejected') : r.reason });
|
|
451
|
+
if (!r.ok)
|
|
452
|
+
return void res.status(409).json({ error: r.reason, error_code: 'DEFERRAL_REJECT_REJECTED' });
|
|
453
|
+
return void res.json({ ok: true, status: r.status, already: !!r.already });
|
|
454
|
+
});
|
|
455
|
+
// ── 按产品认证(per-product verification)审批。读 = ROOT;verify/reject = ROOT + 真人 Passkey(硬门:核验=放行该产品直付,
|
|
456
|
+
// capability-granting → 铁律 Passkey)。reviewProductVerification 是唯一 writer;本文件零 product_verifications 直写。 ──
|
|
457
|
+
const PV_STATUSES = new Set(['issued', 'submitted', 'verified', 'rejected']);
|
|
458
|
+
// GET /api/admin/direct-receive/product-verifications?status=submitted — ROOT 审核队列(默认全部)。纯读。
|
|
459
|
+
app.get('/api/admin/direct-receive/product-verifications', (req, res) => {
|
|
460
|
+
const admin = requireRootAdmin(req, res);
|
|
461
|
+
if (!admin)
|
|
462
|
+
return;
|
|
463
|
+
const status = req.query?.status != null ? String(req.query.status) : '';
|
|
464
|
+
if (status && !PV_STATUSES.has(status))
|
|
465
|
+
return void res.status(400).json({ error: '非法 status', error_code: 'BAD_STATUS' });
|
|
466
|
+
return void res.json({ verifications: listProductVerifications(db, status ? { status: status } : {}) });
|
|
467
|
+
});
|
|
468
|
+
// POST /api/admin/direct-receive/product-verifications/:id/review — ROOT + 真人 Passkey 手动核对结论(verified|rejected)。
|
|
469
|
+
// Passkey purpose_data 绑 verification_id + decision:签 A 用 B / 改结论一律拒。verify = 放行该产品直付(逐品,绝不放行全部)。
|
|
470
|
+
app.post('/api/admin/direct-receive/product-verifications/:id/review', (req, res) => {
|
|
471
|
+
const admin = requireRootAdmin(req, res);
|
|
472
|
+
if (!admin)
|
|
473
|
+
return;
|
|
474
|
+
const verificationId = req.params.id;
|
|
475
|
+
const decision = String(req.body?.decision || '');
|
|
476
|
+
const notes = req.body?.notes != null ? String(req.body.notes) : undefined;
|
|
477
|
+
const webauthnToken = req.body?.webauthn_token;
|
|
478
|
+
const gate = requireDirectPayHumanPasskey({ db, consumeGateToken }, {
|
|
479
|
+
userId: admin.id, webauthnToken, purpose: 'direct_pay_product_verify',
|
|
480
|
+
validate: (data) => { const d = data; return !!d && str(d.verification_id) === verificationId && str(d.decision) === decision; },
|
|
481
|
+
});
|
|
482
|
+
if (!gate.ok) {
|
|
483
|
+
logAdminAction(admin.id, 'direct_pay.product_verify', 'product_verification', verificationId, { decision, ok: false, outcome: gate.error_code === 'PASSKEY_REQUIRED_FOR_DIRECT_PAY' ? 'passkey_required' : 'human_presence_required', error_code: gate.error_code });
|
|
484
|
+
return void res.status(403).json({ error: gate.reason, error_code: gate.error_code });
|
|
485
|
+
}
|
|
486
|
+
const r = reviewProductVerification(db, { id: verificationId, reviewerId: admin.id, decision: decision, notes });
|
|
487
|
+
logAdminAction(admin.id, 'direct_pay.product_verify', 'product_verification', verificationId, { decision, ok: r.ok, outcome: r.ok ? (r.already ? 'already' : r.status) : r.reason });
|
|
488
|
+
if (!r.ok)
|
|
489
|
+
return void res.status(409).json({ error: r.reason, error_code: 'PRODUCT_VERIFICATION_REVIEW_REJECTED' });
|
|
490
|
+
return void res.json({ ok: true, status: r.status, already: !!r.already });
|
|
491
|
+
});
|
|
492
|
+
// ── 店铺认证(per-seller)审批 = 逐品验证豁免决定。读 = ROOT;verify/reject = ROOT + 真人 Passkey。 ──
|
|
493
|
+
// 核店铺时勾选 per_product_exempt:true → 该卖家所有商品免逐品验证(capability-granting → 铁律 Passkey;
|
|
494
|
+
// purpose_data 绑 verification_id+decision+per_product_exempt,签 A 用 B / 改豁免位一律拒)。reviewStoreVerification 唯一 writer。
|
|
495
|
+
const SV_STATUSES = new Set(['issued', 'submitted', 'verified', 'rejected']);
|
|
496
|
+
// GET /api/admin/direct-receive/store-verifications?status=submitted — ROOT 审核队列(默认全部)。纯读。
|
|
497
|
+
app.get('/api/admin/direct-receive/store-verifications', (req, res) => {
|
|
498
|
+
const admin = requireRootAdmin(req, res);
|
|
499
|
+
if (!admin)
|
|
500
|
+
return;
|
|
501
|
+
const status = req.query?.status != null ? String(req.query.status) : '';
|
|
502
|
+
if (status && !SV_STATUSES.has(status))
|
|
503
|
+
return void res.status(400).json({ error: '非法 status', error_code: 'BAD_STATUS' });
|
|
504
|
+
return void res.json({ verifications: listStoreVerifications(db, status ? { status: status } : {}) });
|
|
505
|
+
});
|
|
506
|
+
// POST /api/admin/direct-receive/store-verifications/:id/review — ROOT + 真人 Passkey 核店铺 + 勾选逐品豁免。
|
|
507
|
+
app.post('/api/admin/direct-receive/store-verifications/:id/review', (req, res) => {
|
|
508
|
+
const admin = requireRootAdmin(req, res);
|
|
509
|
+
if (!admin)
|
|
510
|
+
return;
|
|
511
|
+
const verificationId = req.params.id;
|
|
512
|
+
const decision = String(req.body?.decision || '');
|
|
513
|
+
const perProductExempt = req.body?.per_product_exempt === true;
|
|
514
|
+
const notes = req.body?.notes != null ? String(req.body.notes) : undefined;
|
|
515
|
+
const webauthnToken = req.body?.webauthn_token;
|
|
516
|
+
const gate = requireDirectPayHumanPasskey({ db, consumeGateToken }, {
|
|
517
|
+
userId: admin.id, webauthnToken, purpose: 'direct_pay_store_verify',
|
|
518
|
+
validate: (data) => {
|
|
519
|
+
const d = data;
|
|
520
|
+
return !!d && str(d.verification_id) === verificationId && str(d.decision) === decision && (d.per_product_exempt === true) === perProductExempt;
|
|
521
|
+
},
|
|
522
|
+
});
|
|
523
|
+
if (!gate.ok) {
|
|
524
|
+
logAdminAction(admin.id, 'direct_pay.store_verify', 'store_verification', verificationId, { decision, per_product_exempt: perProductExempt, ok: false, outcome: gate.error_code === 'PASSKEY_REQUIRED_FOR_DIRECT_PAY' ? 'passkey_required' : 'human_presence_required', error_code: gate.error_code });
|
|
525
|
+
return void res.status(403).json({ error: gate.reason, error_code: gate.error_code });
|
|
526
|
+
}
|
|
527
|
+
const r = reviewStoreVerification(db, { id: verificationId, reviewerId: admin.id, decision: decision, perProductExempt, notes });
|
|
528
|
+
logAdminAction(admin.id, 'direct_pay.store_verify', 'store_verification', verificationId, { decision, per_product_exempt: perProductExempt, ok: r.ok, outcome: r.ok ? (r.already ? 'already' : r.status) : r.reason });
|
|
529
|
+
if (!r.ok)
|
|
530
|
+
return void res.status(409).json({ error: r.reason, error_code: 'STORE_VERIFICATION_REVIEW_REJECTED' });
|
|
531
|
+
return void res.json({ ok: true, status: r.status, per_product_exempt: !!r.perProductExempt, already: !!r.already });
|
|
532
|
+
});
|
|
533
|
+
}
|
|
@@ -75,6 +75,22 @@ export function registerAdminProtocolParamsRoutes(app, deps) {
|
|
|
75
75
|
if (param.type === 'boolean' && !['true', 'false', '1', '0'].includes(strVal)) {
|
|
76
76
|
return void res.status(400).json({ error: '类型不匹配(需 boolean)' });
|
|
77
77
|
}
|
|
78
|
+
// S1 审计:json 型参数必须能被 parse(此前只有 number/boolean 校验,json 型能写进任意串 → 消费方 fail-open/closed 两难)
|
|
79
|
+
if (param.type === 'json') {
|
|
80
|
+
let parsed;
|
|
81
|
+
try {
|
|
82
|
+
parsed = JSON.parse(strVal);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return void res.status(400).json({ error: '类型不匹配(需合法 JSON)' });
|
|
86
|
+
}
|
|
87
|
+
// key 专项:平台禁售名单必须是大写区码数组(2-8 位字母/数字/-)—— 该参数直接进建单硬门,坏值会 fail-closed 挡全部下单
|
|
88
|
+
if (req.params.key === 'trade.platform_region_blocklist') {
|
|
89
|
+
if (!Array.isArray(parsed) || !parsed.every(x => typeof x === 'string' && /^[A-Z0-9-]{2,8}$/.test(x))) {
|
|
90
|
+
return void res.status(400).json({ error: 'trade.platform_region_blocklist 须为大写区码字符串数组,如 ["KP"]', code: 'BAD_REGION_BLOCKLIST' });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
78
94
|
// A-3: 变更前快照旧值
|
|
79
95
|
const oldRow = await dbOne('SELECT value FROM protocol_params WHERE key = ?', [req.params.key]);
|
|
80
96
|
db.transaction(() => {
|