@seasonkoh/webaz 0.1.7 → 0.1.9
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/LICENSE +48 -0
- package/README.md +156 -20
- package/dist/layer0-foundation/L0-1-database/schema.js +5 -4
- package/dist/layer0-foundation/L0-2-state-machine/engine.js +228 -7
- package/dist/layer0-foundation/L0-2-state-machine/order-chain.js +156 -0
- package/dist/layer0-foundation/L0-2-state-machine/transitions.js +53 -12
- package/dist/layer0-foundation/L0-5-manifest/manifest.js +14 -1
- package/dist/layer1-agent/L1-1-mcp-server/auth.js +1 -1
- package/dist/layer1-agent/L1-1-mcp-server/server.js +3691 -714
- package/dist/layer1-agent/L1-2-external-anchor/anchor-engine.js +324 -0
- package/dist/layer1-agent/L1-2-identity/agent-passport.js +100 -0
- package/dist/layer2-business/L2-6-notifications/notification-engine.js +72 -5
- package/dist/layer2-business/L2-7-snf/snf-engine.js +287 -0
- package/dist/layer2-business/L2-anchor-registry/anchor-registry.js +396 -0
- package/dist/layer2-business/L2-notes/note-photo-storage.js +133 -0
- package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +6 -6
- package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +246 -0
- package/dist/layer4-economics/L4-3-reputation/reputation-engine.js +95 -1
- package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +31 -2
- package/dist/layer4-economics/L4-4-skill-market/skill-listing-engine.js +358 -0
- package/dist/pwa/public/app.js +31947 -0
- package/dist/pwa/public/i18n.js +5751 -0
- package/dist/pwa/public/icon.svg +11 -0
- package/dist/pwa/public/index.html +21 -0
- package/dist/pwa/public/manifest.json +48 -0
- package/dist/pwa/public/openapi.json +5946 -0
- package/dist/pwa/public/style.css +535 -0
- package/dist/pwa/public/sw.js +63 -0
- package/dist/pwa/public/vendor/jsQR.js +10102 -0
- package/dist/pwa/public/webaz-logo.png +0 -0
- package/dist/pwa/routes/account-deletion.js +53 -0
- package/dist/pwa/routes/addresses.js +105 -0
- package/dist/pwa/routes/admin-admins.js +151 -0
- package/dist/pwa/routes/admin-analytics.js +253 -0
- package/dist/pwa/routes/admin-atomic.js +21 -0
- package/dist/pwa/routes/admin-catalog.js +64 -0
- package/dist/pwa/routes/admin-editor-picks.js +45 -0
- package/dist/pwa/routes/admin-events.js +60 -0
- package/dist/pwa/routes/admin-health.js +66 -0
- package/dist/pwa/routes/admin-moderation.js +120 -0
- package/dist/pwa/routes/admin-ops.js +179 -0
- package/dist/pwa/routes/admin-protocol-params.js +79 -0
- package/dist/pwa/routes/admin-reports.js +154 -0
- package/dist/pwa/routes/admin-tokenomics.js +113 -0
- package/dist/pwa/routes/admin-users-lifecycle.js +237 -0
- package/dist/pwa/routes/admin-users-query.js +390 -0
- package/dist/pwa/routes/admin-verifier-flow.js +126 -0
- package/dist/pwa/routes/admin-verifier-whitelist.js +111 -0
- package/dist/pwa/routes/admin-wallet-ops.js +66 -0
- package/dist/pwa/routes/agent-buy.js +215 -0
- package/dist/pwa/routes/agent-governance.js +341 -0
- package/dist/pwa/routes/agent-reputation.js +34 -0
- package/dist/pwa/routes/ai.js +101 -0
- package/dist/pwa/routes/analytics.js +272 -0
- package/dist/pwa/routes/anchors.js +169 -0
- package/dist/pwa/routes/announcements.js +110 -0
- package/dist/pwa/routes/arbitrator.js +117 -0
- package/dist/pwa/routes/auction.js +436 -0
- package/dist/pwa/routes/auth-login.js +40 -0
- package/dist/pwa/routes/auth-read.js +66 -0
- package/dist/pwa/routes/auth-register.js +138 -0
- package/dist/pwa/routes/auth-sessions.js +62 -0
- package/dist/pwa/routes/blocklist.js +60 -0
- package/dist/pwa/routes/buyer-feeds.js +224 -0
- package/dist/pwa/routes/cart.js +155 -0
- package/dist/pwa/routes/charity.js +816 -0
- package/dist/pwa/routes/chat.js +318 -0
- package/dist/pwa/routes/checkin-tasks.js +122 -0
- package/dist/pwa/routes/checkout-helpers.js +85 -0
- package/dist/pwa/routes/claim-initiators.js +88 -0
- package/dist/pwa/routes/claim-verify.js +615 -0
- package/dist/pwa/routes/claim-voting.js +114 -0
- package/dist/pwa/routes/claim-withdrawals.js +20 -0
- package/dist/pwa/routes/coupons.js +165 -0
- package/dist/pwa/routes/dashboards.js +99 -0
- package/dist/pwa/routes/dispute-cases.js +267 -0
- package/dist/pwa/routes/disputes-read.js +358 -0
- package/dist/pwa/routes/disputes-write.js +475 -0
- package/dist/pwa/routes/evidence.js +86 -0
- package/dist/pwa/routes/external-anchors.js +107 -0
- package/dist/pwa/routes/feedback.js +270 -0
- package/dist/pwa/routes/flash-sales.js +130 -0
- package/dist/pwa/routes/follows.js +103 -0
- package/dist/pwa/routes/group-buys.js +208 -0
- package/dist/pwa/routes/growth.js +199 -0
- package/dist/pwa/routes/import-product.js +153 -0
- package/dist/pwa/routes/kyc.js +40 -0
- package/dist/pwa/routes/leaderboard.js +149 -0
- package/dist/pwa/routes/listings.js +281 -0
- package/dist/pwa/routes/logistics.js +35 -0
- package/dist/pwa/routes/manifests.js +126 -0
- package/dist/pwa/routes/me-data.js +101 -0
- package/dist/pwa/routes/notifications.js +48 -0
- package/dist/pwa/routes/offers.js +96 -0
- package/dist/pwa/routes/orders-action.js +285 -0
- package/dist/pwa/routes/orders-create.js +339 -0
- package/dist/pwa/routes/orders-read.js +180 -0
- package/dist/pwa/routes/p2p-products.js +178 -0
- package/dist/pwa/routes/payments-governance.js +311 -0
- package/dist/pwa/routes/peers.js +34 -0
- package/dist/pwa/routes/pin-receipts.js +39 -0
- package/dist/pwa/routes/products-aliases.js +119 -0
- package/dist/pwa/routes/products-claims.js +60 -0
- package/dist/pwa/routes/products-create.js +206 -0
- package/dist/pwa/routes/products-crud.js +73 -0
- package/dist/pwa/routes/products-links.js +129 -0
- package/dist/pwa/routes/products-list.js +424 -0
- package/dist/pwa/routes/products-meta.js +155 -0
- package/dist/pwa/routes/products-update.js +125 -0
- package/dist/pwa/routes/profile-credentials.js +105 -0
- package/dist/pwa/routes/profile-identity.js +174 -0
- package/dist/pwa/routes/profile-location.js +35 -0
- package/dist/pwa/routes/profile-placement.js +70 -0
- package/dist/pwa/routes/profile-prefs.js +93 -0
- package/dist/pwa/routes/promoter.js +208 -0
- package/dist/pwa/routes/public-utils.js +170 -0
- package/dist/pwa/routes/push.js +54 -0
- package/dist/pwa/routes/ratings.js +220 -0
- package/dist/pwa/routes/recover-key.js +100 -0
- package/dist/pwa/routes/referral.js +58 -0
- package/dist/pwa/routes/reputation.js +34 -0
- package/dist/pwa/routes/returns.js +493 -0
- package/dist/pwa/routes/reviews.js +81 -0
- package/dist/pwa/routes/rfqs.js +443 -0
- package/dist/pwa/routes/search.js +172 -0
- package/dist/pwa/routes/secondhand.js +278 -0
- package/dist/pwa/routes/seller-quota.js +225 -0
- package/dist/pwa/routes/share-redirects.js +164 -0
- package/dist/pwa/routes/shareables-interactions.js +212 -0
- package/dist/pwa/routes/shareables.js +470 -0
- package/dist/pwa/routes/shops.js +98 -0
- package/dist/pwa/routes/signaling.js +43 -0
- package/dist/pwa/routes/skill-market.js +173 -0
- package/dist/pwa/routes/skills.js +174 -0
- package/dist/pwa/routes/snf.js +126 -0
- package/dist/pwa/routes/tags.js +47 -0
- package/dist/pwa/routes/trial.js +333 -0
- package/dist/pwa/routes/trusted-kpi.js +87 -0
- package/dist/pwa/routes/url-claim.js +113 -0
- package/dist/pwa/routes/users-public.js +317 -0
- package/dist/pwa/routes/variants.js +156 -0
- package/dist/pwa/routes/verifier-user.js +107 -0
- package/dist/pwa/routes/verify-tasks.js +120 -0
- package/dist/pwa/routes/waitlist.js +65 -0
- package/dist/pwa/routes/wallet-read.js +218 -0
- package/dist/pwa/routes/wallet-write.js +273 -0
- package/dist/pwa/routes/webauthn.js +188 -0
- package/dist/pwa/routes/webhooks.js +162 -0
- package/dist/pwa/routes/welcome.js +226 -0
- package/dist/pwa/routes/wishlist-qa.js +135 -0
- package/dist/pwa/security/ssrf.js +110 -0
- package/dist/pwa/server.js +9679 -698
- package/package.json +11 -4
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
export function registerAdminReportsRoutes(app, deps) {
|
|
2
|
+
const { db, requireContentAdmin, requireArbitrationAdmin, requireProtocolAdmin } = deps;
|
|
3
|
+
app.get('/api/admin/orders', (req, res) => {
|
|
4
|
+
const admin = requireContentAdmin(req, res);
|
|
5
|
+
if (!admin)
|
|
6
|
+
return;
|
|
7
|
+
const status = req.query.status;
|
|
8
|
+
let sql = `SELECT o.id, o.product_id, o.buyer_id, o.seller_id, o.logistics_id, o.status,
|
|
9
|
+
o.total_amount, o.created_at,
|
|
10
|
+
p.title as product_title,
|
|
11
|
+
ub.name as buyer_name, us.name as seller_name
|
|
12
|
+
FROM orders o
|
|
13
|
+
JOIN products p ON o.product_id = p.id
|
|
14
|
+
JOIN users ub ON o.buyer_id = ub.id
|
|
15
|
+
JOIN users us ON o.seller_id = us.id`;
|
|
16
|
+
const params = [];
|
|
17
|
+
if (status && status.trim()) {
|
|
18
|
+
sql += ` WHERE o.status = ?`;
|
|
19
|
+
params.push(status);
|
|
20
|
+
}
|
|
21
|
+
sql += ` ORDER BY o.created_at DESC LIMIT 100`;
|
|
22
|
+
res.json({ orders: db.prepare(sql).all(...params) });
|
|
23
|
+
});
|
|
24
|
+
app.get('/api/admin/disputes', (req, res) => {
|
|
25
|
+
const admin = requireArbitrationAdmin(req, res);
|
|
26
|
+
if (!admin)
|
|
27
|
+
return;
|
|
28
|
+
const rows = db.prepare(`
|
|
29
|
+
SELECT d.id, d.order_id, d.initiator_id, d.defendant_id, d.reason, d.status,
|
|
30
|
+
d.created_at, d.respond_deadline, d.arbitrate_deadline, d.resolved_at,
|
|
31
|
+
u1.name as initiator_name, u2.name as defendant_name,
|
|
32
|
+
o.total_amount, o.status as order_status,
|
|
33
|
+
p.title as product_title
|
|
34
|
+
FROM disputes d
|
|
35
|
+
LEFT JOIN users u1 ON d.initiator_id = u1.id
|
|
36
|
+
LEFT JOIN users u2 ON d.defendant_id = u2.id
|
|
37
|
+
LEFT JOIN orders o ON d.order_id = o.id
|
|
38
|
+
LEFT JOIN products p ON o.product_id = p.id
|
|
39
|
+
ORDER BY d.created_at DESC LIMIT 100
|
|
40
|
+
`).all();
|
|
41
|
+
res.json({ disputes: rows });
|
|
42
|
+
});
|
|
43
|
+
app.get('/api/admin/verify-tasks', (req, res) => {
|
|
44
|
+
const admin = requireArbitrationAdmin(req, res);
|
|
45
|
+
if (!admin)
|
|
46
|
+
return;
|
|
47
|
+
const status = req.query.status;
|
|
48
|
+
let sql = `SELECT vt.id, vt.type, vt.status, vt.url, vt.code, vt.result, vt.fee_locked,
|
|
49
|
+
vt.verifiers_needed, vt.created_at, vt.expires_at, vt.settled_at,
|
|
50
|
+
vt.product_id, p.title as product_title, p.seller_id, u.name as seller_name
|
|
51
|
+
FROM verify_tasks vt
|
|
52
|
+
LEFT JOIN products p ON vt.product_id = p.id
|
|
53
|
+
LEFT JOIN users u ON p.seller_id = u.id`;
|
|
54
|
+
const params = [];
|
|
55
|
+
if (status && status.trim()) {
|
|
56
|
+
sql += ` WHERE vt.status = ?`;
|
|
57
|
+
params.push(status);
|
|
58
|
+
}
|
|
59
|
+
sql += ` ORDER BY vt.created_at DESC LIMIT 100`;
|
|
60
|
+
res.json({ tasks: db.prepare(sql).all(...params) });
|
|
61
|
+
});
|
|
62
|
+
// 双引擎收入治理视图 — 三级奖励(推土机) vs PV 对碰(原子能) 协议级聚合
|
|
63
|
+
// 治理依据:根据两引擎实际拨付量决定费率/阈值/拨出率等调参方案
|
|
64
|
+
// 隐私第一:运营财务,仅 protocol admin 可见(详见 docs/REWARD-ENGINES-DECOUPLING.md)
|
|
65
|
+
app.get('/api/admin/economic-summary', (req, res) => {
|
|
66
|
+
const admin = requireProtocolAdmin(req, res);
|
|
67
|
+
if (!admin)
|
|
68
|
+
return;
|
|
69
|
+
const r2 = (n) => Math.round(Number(n || 0) * 100) / 100;
|
|
70
|
+
// 引擎 A:三级奖励(commission_records 实际分账,按 level)
|
|
71
|
+
const commByLevel = db.prepare(`
|
|
72
|
+
SELECT level, COUNT(*) AS cnt, COALESCE(SUM(amount),0) AS total
|
|
73
|
+
FROM commission_records GROUP BY level
|
|
74
|
+
`).all();
|
|
75
|
+
const engineA = { l1: { count: 0, total: 0 }, l2: { count: 0, total: 0 }, l3: { count: 0, total: 0 }, distributed_total: 0 };
|
|
76
|
+
for (const r of commByLevel) {
|
|
77
|
+
const k = `l${r.level}`;
|
|
78
|
+
if (engineA[k]) {
|
|
79
|
+
engineA[k] = { count: r.cnt, total: r2(r.total) };
|
|
80
|
+
engineA.distributed_total += Number(r.total);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
engineA.distributed_total = r2(engineA.distributed_total);
|
|
84
|
+
// 引擎 A 的 redirect 去向(chain_gap → charity;region_cap → global_fund)
|
|
85
|
+
const charity = db.prepare(`SELECT balance, total_chain_gap, total_orphan_sponsor, total_region_cap, total_donated, total_disbursed FROM charity_fund WHERE id='main'`).get();
|
|
86
|
+
// 引擎 B:PV 对碰(binary_score_records 现金拨付 + 待拨 Score)
|
|
87
|
+
const binarySettled = db.prepare(`SELECT COUNT(*) AS cnt, COALESCE(SUM(waz_amount),0) AS waz FROM binary_score_records WHERE settled_at IS NOT NULL`).get();
|
|
88
|
+
const binaryPending = db.prepare(`SELECT COUNT(*) AS cnt, COALESCE(SUM(score),0) AS score FROM binary_score_records WHERE settled_at IS NULL`).get();
|
|
89
|
+
const periods = db.prepare(`SELECT COUNT(*) AS cnt, COALESCE(SUM(cash_distributed),0) AS dist, COALESCE(SUM(cash_retained),0) AS retained FROM settlement_periods WHERE status='completed'`).get();
|
|
90
|
+
const gfund = db.prepare(`SELECT pool_balance, total_scores_pending, current_n FROM global_fund WHERE id=1`).get();
|
|
91
|
+
// 资金管道:global_fund 蓄水来源(fund_base 1% + commission region_cap redirect)
|
|
92
|
+
const pipe = db.prepare(`SELECT COALESCE(SUM(amount_base),0) AS base, COALESCE(SUM(amount_l3),0) AS redirect FROM fund_deposits`).get();
|
|
93
|
+
res.json({
|
|
94
|
+
engine_a_commission: {
|
|
95
|
+
...engineA,
|
|
96
|
+
// redirect 三去向(互斥,全部源自 commission_pool 未发出的份额):
|
|
97
|
+
redirected_to_charity_chain_gap: r2(Number(charity?.total_chain_gap || 0) + Number(charity?.total_orphan_sponsor || 0)), // 无 L / sponsor 无效
|
|
98
|
+
redirected_to_charity_region_full_ban: r2(Number(charity?.total_region_cap || 0)), // max_levels=0 区域整池入公益
|
|
99
|
+
redirected_to_global_fund_region_cap: r2(pipe.redirect), // level > max_levels(部分截断)→ global_fund (fund_deposits.amount_l3)
|
|
100
|
+
note: '即时分账,bounded by commission_pool(订单内)。redirect 三去向见上字段。',
|
|
101
|
+
},
|
|
102
|
+
engine_b_pv_matching: {
|
|
103
|
+
cash_distributed_total: r2(binarySettled.waz),
|
|
104
|
+
settled_match_count: binarySettled.cnt,
|
|
105
|
+
pending_score: r2(binaryPending.score),
|
|
106
|
+
pending_match_count: binaryPending.cnt,
|
|
107
|
+
settlement_periods_completed: periods.cnt,
|
|
108
|
+
periods_cash_distributed: r2(periods.dist),
|
|
109
|
+
periods_cash_retained: r2(periods.retained),
|
|
110
|
+
note: 'Score → 现金经安全阀拨付,源自 global_fund,永不透支。',
|
|
111
|
+
},
|
|
112
|
+
global_fund: {
|
|
113
|
+
pool_balance: r2(Number(gfund?.pool_balance || 0)),
|
|
114
|
+
total_scores_pending: r2(Number(gfund?.total_scores_pending || 0)),
|
|
115
|
+
current_n: r2(Number(gfund?.current_n || 0)),
|
|
116
|
+
},
|
|
117
|
+
funding_pipe: {
|
|
118
|
+
fund_base_1pct_accumulated: r2(pipe.base),
|
|
119
|
+
commission_redirect_accumulated: r2(pipe.redirect),
|
|
120
|
+
note: 'global_fund 蓄水来源(单向):每单 1% fund_base + commission region_cap redirect。',
|
|
121
|
+
},
|
|
122
|
+
charity_fund: {
|
|
123
|
+
balance: r2(Number(charity?.balance || 0)),
|
|
124
|
+
total_donated: r2(Number(charity?.total_donated || 0)),
|
|
125
|
+
total_disbursed: r2(Number(charity?.total_disbursed || 0)),
|
|
126
|
+
},
|
|
127
|
+
governance_hint: '引擎A=消费即时奖励(commission_pool);引擎B=团队对碰(global_fund 安全阀)。两者解耦,调参互不影响。详见 docs/REWARD-ENGINES-DECOUPLING.md',
|
|
128
|
+
generated_at: new Date().toISOString(),
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
app.get('/api/admin/audit-log', (req, res) => {
|
|
132
|
+
const admin = requireProtocolAdmin(req, res);
|
|
133
|
+
if (!admin)
|
|
134
|
+
return;
|
|
135
|
+
const rows = db.prepare(`
|
|
136
|
+
SELECT al.id, al.admin_id, al.action, al.target_type, al.target_id, al.detail, al.created_at,
|
|
137
|
+
u.name as admin_name
|
|
138
|
+
FROM admin_audit_log al
|
|
139
|
+
LEFT JOIN users u ON u.id = al.admin_id
|
|
140
|
+
ORDER BY al.created_at DESC LIMIT 50
|
|
141
|
+
`).all();
|
|
142
|
+
res.json({
|
|
143
|
+
entries: rows.map(r => ({
|
|
144
|
+
...r,
|
|
145
|
+
detail: r.detail ? (() => { try {
|
|
146
|
+
return JSON.parse(r.detail);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return r.detail;
|
|
150
|
+
} })() : null,
|
|
151
|
+
})),
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
export function registerAdminTokenomicsRoutes(app, deps) {
|
|
2
|
+
const { db, requireProtocolAdmin, logAdminAction } = deps;
|
|
3
|
+
// Tokenomics 详细数据 + Tier 配置 + 高额榜
|
|
4
|
+
app.get('/api/admin/tokenomics', (req, res) => {
|
|
5
|
+
const admin = requireProtocolAdmin(req, res);
|
|
6
|
+
if (!admin)
|
|
7
|
+
return;
|
|
8
|
+
const tiers = db.prepare("SELECT * FROM binary_tier_config ORDER BY tier ASC").all();
|
|
9
|
+
const topComm = db.prepare(`
|
|
10
|
+
SELECT cr.beneficiary_id, u.name, COUNT(*) as records, COALESCE(SUM(cr.amount),0) as earned
|
|
11
|
+
FROM commission_records cr LEFT JOIN users u ON u.id = cr.beneficiary_id
|
|
12
|
+
WHERE cr.beneficiary_id != 'sys_protocol'
|
|
13
|
+
GROUP BY cr.beneficiary_id ORDER BY earned DESC LIMIT 10
|
|
14
|
+
`).all();
|
|
15
|
+
const topBinary = db.prepare(`
|
|
16
|
+
SELECT bsr.user_id, u.name,
|
|
17
|
+
COUNT(*) as hits,
|
|
18
|
+
COALESCE(SUM(CASE WHEN settled_at IS NOT NULL THEN waz_amount ELSE 0 END),0) as waz_total,
|
|
19
|
+
COALESCE(SUM(score),0) as score_total
|
|
20
|
+
FROM binary_score_records bsr LEFT JOIN users u ON u.id = bsr.user_id
|
|
21
|
+
GROUP BY bsr.user_id ORDER BY waz_total DESC LIMIT 10
|
|
22
|
+
`).all();
|
|
23
|
+
const gf = db.prepare("SELECT * FROM global_fund WHERE id=1").get();
|
|
24
|
+
const mb = db.prepare("SELECT balance FROM management_bonus_pool WHERE id=1").get();
|
|
25
|
+
const pvLedger = db.prepare(`
|
|
26
|
+
SELECT COUNT(*) as total,
|
|
27
|
+
SUM(CASE WHEN processed=0 THEN 1 ELSE 0 END) as pending,
|
|
28
|
+
COALESCE(SUM(CASE WHEN processed=0 THEN pv ELSE 0 END),0) as pending_pv
|
|
29
|
+
FROM pv_ledger
|
|
30
|
+
`).get();
|
|
31
|
+
res.json({
|
|
32
|
+
global_fund: gf,
|
|
33
|
+
management_bonus_pool: mb,
|
|
34
|
+
tier_config: tiers,
|
|
35
|
+
pv_ledger: pvLedger,
|
|
36
|
+
top_commission: topComm,
|
|
37
|
+
top_binary: topBinary,
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
// 调整 Tier 配置
|
|
41
|
+
app.post('/api/admin/tokenomics/tier', (req, res) => {
|
|
42
|
+
const admin = requireProtocolAdmin(req, res);
|
|
43
|
+
if (!admin)
|
|
44
|
+
return;
|
|
45
|
+
const { tier, pv_threshold, score_per_hit, active } = req.body;
|
|
46
|
+
if (!Number.isInteger(tier) || tier < 1 || tier > 10)
|
|
47
|
+
return void res.json({ error: 'tier 无效' });
|
|
48
|
+
if (Number(pv_threshold) <= 0 || Number(score_per_hit) <= 0)
|
|
49
|
+
return void res.json({ error: '阈值/分数必须 > 0' });
|
|
50
|
+
db.prepare(`INSERT OR REPLACE INTO binary_tier_config (tier, pv_threshold, score_per_hit, active) VALUES (?,?,?,?)`)
|
|
51
|
+
.run(tier, Number(pv_threshold), Number(score_per_hit), active ? 1 : 0);
|
|
52
|
+
logAdminAction(admin.id, 'tokenomics_tier_update', 'tier', String(tier), { pv_threshold, score_per_hit, active });
|
|
53
|
+
res.json({ success: true });
|
|
54
|
+
});
|
|
55
|
+
// 管理津贴资格列表 + 开关
|
|
56
|
+
app.get('/api/admin/tokenomics/mgmt-bonus', (req, res) => {
|
|
57
|
+
const admin = requireProtocolAdmin(req, res);
|
|
58
|
+
if (!admin)
|
|
59
|
+
return;
|
|
60
|
+
const enabled = db.prepare("SELECT value FROM system_state WHERE key='mgmt_bonus_enabled'").get()?.value === '1';
|
|
61
|
+
const eligible = db.prepare(`
|
|
62
|
+
SELECT u.id, u.name, u.created_at,
|
|
63
|
+
(SELECT COUNT(*) FROM users WHERE sponsor_id = u.id) as l1_count,
|
|
64
|
+
COALESCE((SELECT SUM(amount) FROM commission_records WHERE beneficiary_id = u.id),0) as total_commission
|
|
65
|
+
FROM users u WHERE u.mgmt_bonus_eligible = 1
|
|
66
|
+
ORDER BY u.created_at DESC LIMIT 100
|
|
67
|
+
`).all();
|
|
68
|
+
res.json({ enabled, eligible_users: eligible, eligible_count: eligible.length });
|
|
69
|
+
});
|
|
70
|
+
// 注册必须 ref 开关
|
|
71
|
+
app.post('/api/admin/tokenomics/require-ref/toggle', (req, res) => {
|
|
72
|
+
const admin = requireProtocolAdmin(req, res);
|
|
73
|
+
if (!admin)
|
|
74
|
+
return;
|
|
75
|
+
const { enabled } = req.body;
|
|
76
|
+
const v = enabled ? '1' : '0';
|
|
77
|
+
db.prepare("INSERT OR REPLACE INTO system_state (key, value) VALUES ('require_ref_to_register', ?)").run(v);
|
|
78
|
+
logAdminAction(admin.id, 'require_ref_toggle', 'system', 'require_ref_to_register', { value: v });
|
|
79
|
+
res.json({ success: true, enabled: !!enabled });
|
|
80
|
+
});
|
|
81
|
+
// 管理津贴池开关
|
|
82
|
+
app.post('/api/admin/tokenomics/mgmt-bonus/toggle', (req, res) => {
|
|
83
|
+
const admin = requireProtocolAdmin(req, res);
|
|
84
|
+
if (!admin)
|
|
85
|
+
return;
|
|
86
|
+
const { enabled } = req.body;
|
|
87
|
+
const v = enabled ? '1' : '0';
|
|
88
|
+
db.prepare("INSERT OR REPLACE INTO system_state (key, value) VALUES ('mgmt_bonus_enabled', ?)").run(v);
|
|
89
|
+
logAdminAction(admin.id, 'mgmt_bonus_toggle', 'system', 'mgmt_bonus_enabled', { value: v });
|
|
90
|
+
res.json({ success: true, enabled: !!enabled });
|
|
91
|
+
});
|
|
92
|
+
// 池注资
|
|
93
|
+
app.post('/api/admin/tokenomics/inject', (req, res) => {
|
|
94
|
+
const admin = requireProtocolAdmin(req, res);
|
|
95
|
+
if (!admin)
|
|
96
|
+
return;
|
|
97
|
+
const { pool, amount } = req.body;
|
|
98
|
+
const n = Number(amount);
|
|
99
|
+
if (!(n > 0))
|
|
100
|
+
return void res.json({ error: '金额必须 > 0' });
|
|
101
|
+
if (pool === 'global_fund') {
|
|
102
|
+
db.prepare("UPDATE global_fund SET pool_balance = pool_balance + ? WHERE id=1").run(n);
|
|
103
|
+
}
|
|
104
|
+
else if (pool === 'management_bonus') {
|
|
105
|
+
db.prepare("UPDATE management_bonus_pool SET balance = balance + ? WHERE id=1").run(n);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
return void res.json({ error: 'pool 名称无效(global_fund / management_bonus)' });
|
|
109
|
+
}
|
|
110
|
+
logAdminAction(admin.id, 'tokenomics_pool_inject', 'pool', pool, { amount: n });
|
|
111
|
+
res.json({ success: true });
|
|
112
|
+
});
|
|
113
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
export function registerAdminUsersLifecycleRoutes(app, deps) {
|
|
2
|
+
const { db, requireUsersAdmin, requireProtocolAdmin, requireContentAdmin, requireRootAdmin, adminCanOperateOn, isRootAdmin, safeRoles, logAdminAction, QUOTA_TIERS } = deps;
|
|
3
|
+
// L1 分享权限 override:0 auto / 1 强允 / -1 强禁
|
|
4
|
+
app.post('/api/admin/users/:id/l1-share-override', (req, res) => {
|
|
5
|
+
const admin = requireProtocolAdmin(req, res);
|
|
6
|
+
if (!admin)
|
|
7
|
+
return;
|
|
8
|
+
if (!adminCanOperateOn(admin, req.params.id, res))
|
|
9
|
+
return;
|
|
10
|
+
const { value, note } = req.body;
|
|
11
|
+
const v = Number(value);
|
|
12
|
+
if (![0, 1, -1].includes(v))
|
|
13
|
+
return void res.json({ error: 'value 必须是 0 / 1 / -1' });
|
|
14
|
+
const target = db.prepare("SELECT id FROM users WHERE id = ?").get(req.params.id);
|
|
15
|
+
if (!target)
|
|
16
|
+
return void res.json({ error: '用户不存在' });
|
|
17
|
+
db.prepare("UPDATE users SET l1_share_override = ?, updated_at = datetime('now') WHERE id = ?").run(v, req.params.id);
|
|
18
|
+
logAdminAction(admin.id, 'l1_share_override', 'user', req.params.id, { value: v, note: note || null });
|
|
19
|
+
res.json({ success: true, value: v });
|
|
20
|
+
});
|
|
21
|
+
app.post('/api/admin/users/:id/mgmt-bonus-eligible', (req, res) => {
|
|
22
|
+
const admin = requireProtocolAdmin(req, res);
|
|
23
|
+
if (!admin)
|
|
24
|
+
return;
|
|
25
|
+
if (!adminCanOperateOn(admin, req.params.id, res))
|
|
26
|
+
return;
|
|
27
|
+
const { eligible, note } = req.body;
|
|
28
|
+
const target = db.prepare("SELECT id, name FROM users WHERE id = ?").get(req.params.id);
|
|
29
|
+
if (!target)
|
|
30
|
+
return void res.json({ error: '用户不存在' });
|
|
31
|
+
db.prepare("UPDATE users SET mgmt_bonus_eligible = ?, updated_at = datetime('now') WHERE id = ?").run(eligible ? 1 : 0, req.params.id);
|
|
32
|
+
logAdminAction(admin.id, eligible ? 'mgmt_bonus_grant' : 'mgmt_bonus_revoke', 'user', req.params.id, { note: note || null });
|
|
33
|
+
res.json({ success: true });
|
|
34
|
+
});
|
|
35
|
+
app.post('/api/admin/users/:id/reset-failed-attempts', (req, res) => {
|
|
36
|
+
const admin = requireUsersAdmin(req, res);
|
|
37
|
+
if (!admin)
|
|
38
|
+
return;
|
|
39
|
+
if (!adminCanOperateOn(admin, req.params.id, res))
|
|
40
|
+
return;
|
|
41
|
+
db.prepare("UPDATE users SET failed_attempts = 0, locked_until = NULL WHERE id = ?").run(req.params.id);
|
|
42
|
+
logAdminAction(admin.id, 'reset_failed_attempts', 'user', req.params.id, {});
|
|
43
|
+
res.json({ success: true });
|
|
44
|
+
});
|
|
45
|
+
app.post('/api/admin/users/:id/force-delist-all', (req, res) => {
|
|
46
|
+
// P0.5: content 权限 + scope
|
|
47
|
+
const admin = requireContentAdmin(req, res);
|
|
48
|
+
if (!admin)
|
|
49
|
+
return;
|
|
50
|
+
if (!adminCanOperateOn(admin, req.params.id, res))
|
|
51
|
+
return;
|
|
52
|
+
const { reason } = req.body;
|
|
53
|
+
const seller = db.prepare("SELECT id, name FROM users WHERE id = ?").get(req.params.id);
|
|
54
|
+
if (!seller)
|
|
55
|
+
return void res.json({ error: '用户不存在' });
|
|
56
|
+
const result = db.prepare("UPDATE products SET status = 'paused', updated_at = datetime('now') WHERE seller_id = ? AND status = 'active'").run(req.params.id);
|
|
57
|
+
logAdminAction(admin.id, 'force_delist_all', 'user', req.params.id, { reason: reason || null, count: result.changes });
|
|
58
|
+
res.json({ success: true, count: result.changes });
|
|
59
|
+
});
|
|
60
|
+
// P0.4: users + scope;suspend admin → root only
|
|
61
|
+
app.post('/api/admin/users/:id/suspend', (req, res) => {
|
|
62
|
+
const admin = requireUsersAdmin(req, res);
|
|
63
|
+
if (!admin)
|
|
64
|
+
return;
|
|
65
|
+
const targetId = req.params.id;
|
|
66
|
+
const { reason } = req.body;
|
|
67
|
+
if (targetId === admin.id)
|
|
68
|
+
return void res.json({ error: '不能暂停自己' });
|
|
69
|
+
const target = db.prepare("SELECT id, role, region FROM users WHERE id = ?").get(targetId);
|
|
70
|
+
if (!target)
|
|
71
|
+
return void res.json({ error: '用户不存在' });
|
|
72
|
+
if (target.role === 'admin') {
|
|
73
|
+
if (!isRootAdmin(admin))
|
|
74
|
+
return void res.status(403).json({ error: '仅 root 可暂停其他 admin(或先撤销其 admin 角色)' });
|
|
75
|
+
}
|
|
76
|
+
if (target.role !== 'admin' && !adminCanOperateOn(admin, targetId, res))
|
|
77
|
+
return;
|
|
78
|
+
db.prepare(`INSERT INTO user_moderation (user_id, suspended, reason, suspended_by, suspended_at)
|
|
79
|
+
VALUES (?, 1, ?, ?, datetime('now'))
|
|
80
|
+
ON CONFLICT(user_id) DO UPDATE SET
|
|
81
|
+
suspended = 1,
|
|
82
|
+
reason = excluded.reason,
|
|
83
|
+
suspended_by = excluded.suspended_by,
|
|
84
|
+
suspended_at = datetime('now')`)
|
|
85
|
+
.run(targetId, reason || null, admin.id);
|
|
86
|
+
logAdminAction(admin.id, 'suspend_user', 'user', targetId, { reason: reason || null });
|
|
87
|
+
res.json({ success: true });
|
|
88
|
+
});
|
|
89
|
+
app.post('/api/admin/users/:id/unsuspend', (req, res) => {
|
|
90
|
+
const admin = requireUsersAdmin(req, res);
|
|
91
|
+
if (!admin)
|
|
92
|
+
return;
|
|
93
|
+
const targetId = req.params.id;
|
|
94
|
+
const target = db.prepare("SELECT id, role FROM users WHERE id = ?").get(targetId);
|
|
95
|
+
if (!target)
|
|
96
|
+
return void res.json({ error: '用户不存在' });
|
|
97
|
+
if (target.role === 'admin' && !isRootAdmin(admin))
|
|
98
|
+
return void res.status(403).json({ error: '仅 root 可恢复其他 admin' });
|
|
99
|
+
if (target.role !== 'admin' && !adminCanOperateOn(admin, targetId, res))
|
|
100
|
+
return;
|
|
101
|
+
db.prepare("UPDATE user_moderation SET suspended = 0, suspended_at = NULL WHERE user_id = ?").run(targetId);
|
|
102
|
+
logAdminAction(admin.id, 'unsuspend_user', 'user', targetId, {});
|
|
103
|
+
res.json({ success: true });
|
|
104
|
+
});
|
|
105
|
+
// P0.1: admin 角色提权必须 root;其他角色需 users + scope
|
|
106
|
+
app.post('/api/admin/users/:id/grant-role', (req, res) => {
|
|
107
|
+
const { role } = req.body;
|
|
108
|
+
const allowed = ['admin', 'verifier', 'arbitrator', 'logistics', 'seller', 'buyer'];
|
|
109
|
+
if (!allowed.includes(role))
|
|
110
|
+
return void res.json({ error: '角色无效' });
|
|
111
|
+
const admin = role === 'admin' ? requireRootAdmin(req, res) : requireUsersAdmin(req, res);
|
|
112
|
+
if (!admin)
|
|
113
|
+
return;
|
|
114
|
+
const targetId = req.params.id;
|
|
115
|
+
if (!adminCanOperateOn(admin, targetId, res))
|
|
116
|
+
return;
|
|
117
|
+
const target = db.prepare("SELECT id, roles FROM users WHERE id = ?").get(targetId);
|
|
118
|
+
if (!target)
|
|
119
|
+
return void res.json({ error: '用户不存在' });
|
|
120
|
+
const roles = safeRoles(target);
|
|
121
|
+
if (roles.includes(role))
|
|
122
|
+
return void res.json({ error: '该用户已拥有此角色' });
|
|
123
|
+
roles.push(role);
|
|
124
|
+
db.prepare("UPDATE users SET roles = ?, updated_at = datetime('now') WHERE id = ?")
|
|
125
|
+
.run(JSON.stringify(roles), targetId);
|
|
126
|
+
logAdminAction(admin.id, 'grant_role', 'user', targetId, { role });
|
|
127
|
+
res.json({ success: true, roles });
|
|
128
|
+
});
|
|
129
|
+
// P0.2: preview diff,含 admin 变更 → root only
|
|
130
|
+
app.post('/api/admin/users/:id/set-roles', (req, res) => {
|
|
131
|
+
const { roles } = req.body;
|
|
132
|
+
const allowed = ['buyer', 'seller', 'logistics', 'arbitrator', 'verifier', 'admin'];
|
|
133
|
+
if (!Array.isArray(roles) || roles.length === 0)
|
|
134
|
+
return void res.json({ error: '至少保留一个角色' });
|
|
135
|
+
for (const r of roles) {
|
|
136
|
+
if (!allowed.includes(r))
|
|
137
|
+
return void res.json({ error: `角色 ${r} 无效` });
|
|
138
|
+
}
|
|
139
|
+
const dedup = Array.from(new Set(roles));
|
|
140
|
+
const targetId = req.params.id;
|
|
141
|
+
const target = db.prepare("SELECT id, role, roles FROM users WHERE id = ?").get(targetId);
|
|
142
|
+
if (!target)
|
|
143
|
+
return void res.json({ error: '用户不存在' });
|
|
144
|
+
const oldRoles = (() => { try {
|
|
145
|
+
return JSON.parse(target.roles || '[]');
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return [];
|
|
149
|
+
} })();
|
|
150
|
+
const added = dedup.filter(r => !oldRoles.includes(r));
|
|
151
|
+
const removed = oldRoles.filter(r => !dedup.includes(r));
|
|
152
|
+
const involvesAdmin = added.includes('admin') || removed.includes('admin');
|
|
153
|
+
const admin = involvesAdmin ? requireRootAdmin(req, res) : requireUsersAdmin(req, res);
|
|
154
|
+
if (!admin)
|
|
155
|
+
return;
|
|
156
|
+
if (!adminCanOperateOn(admin, targetId, res))
|
|
157
|
+
return;
|
|
158
|
+
if (targetId === admin.id && removed.includes('admin')) {
|
|
159
|
+
return void res.json({ error: '不能撤销自己的管理员角色' });
|
|
160
|
+
}
|
|
161
|
+
if (added.length === 0 && removed.length === 0) {
|
|
162
|
+
return void res.json({ error: '角色无变更' });
|
|
163
|
+
}
|
|
164
|
+
const newActiveRole = dedup.includes(target.role) ? target.role : dedup[0];
|
|
165
|
+
db.prepare("UPDATE users SET role = ?, roles = ?, updated_at = datetime('now') WHERE id = ?")
|
|
166
|
+
.run(newActiveRole, JSON.stringify(dedup), targetId);
|
|
167
|
+
if (added.length)
|
|
168
|
+
logAdminAction(admin.id, 'grant_role_batch', 'user', targetId, { roles: added });
|
|
169
|
+
if (removed.length)
|
|
170
|
+
logAdminAction(admin.id, 'revoke_role_batch', 'user', targetId, { roles: removed });
|
|
171
|
+
res.json({ success: true, roles: dedup, added, removed });
|
|
172
|
+
});
|
|
173
|
+
// P0.3: revoke admin → root only
|
|
174
|
+
app.post('/api/admin/users/:id/revoke-role', (req, res) => {
|
|
175
|
+
const { role } = req.body;
|
|
176
|
+
const admin = role === 'admin' ? requireRootAdmin(req, res) : requireUsersAdmin(req, res);
|
|
177
|
+
if (!admin)
|
|
178
|
+
return;
|
|
179
|
+
const targetId = req.params.id;
|
|
180
|
+
if (!adminCanOperateOn(admin, targetId, res))
|
|
181
|
+
return;
|
|
182
|
+
if (targetId === admin.id && role === 'admin')
|
|
183
|
+
return void res.json({ error: '不能撤销自己的管理员角色(防自杀)' });
|
|
184
|
+
const target = db.prepare("SELECT id, role, roles FROM users WHERE id = ?").get(targetId);
|
|
185
|
+
if (!target)
|
|
186
|
+
return void res.json({ error: '用户不存在' });
|
|
187
|
+
const roles = safeRoles(target).filter(r => r !== role);
|
|
188
|
+
if (roles.length === 0)
|
|
189
|
+
return void res.json({ error: '用户至少保留一个角色' });
|
|
190
|
+
const newActiveRole = target.role === role ? roles[0] : target.role;
|
|
191
|
+
db.prepare("UPDATE users SET role = ?, roles = ?, updated_at = datetime('now') WHERE id = ?")
|
|
192
|
+
.run(newActiveRole, JSON.stringify(roles), targetId);
|
|
193
|
+
logAdminAction(admin.id, 'revoke_role', 'user', targetId, { role });
|
|
194
|
+
res.json({ success: true, roles });
|
|
195
|
+
});
|
|
196
|
+
app.post('/api/admin/users/:id/set-product-quota', (req, res) => {
|
|
197
|
+
const admin = requireUsersAdmin(req, res);
|
|
198
|
+
if (!admin)
|
|
199
|
+
return;
|
|
200
|
+
if (!adminCanOperateOn(admin, req.params.id, res))
|
|
201
|
+
return;
|
|
202
|
+
const { max_products } = req.body;
|
|
203
|
+
const n = Number(max_products);
|
|
204
|
+
if (!QUOTA_TIERS.includes(n))
|
|
205
|
+
return void res.json({ error: `配额应为 ${QUOTA_TIERS.join(' / ')} 之一` });
|
|
206
|
+
const target = db.prepare("SELECT id FROM users WHERE id = ?").get(req.params.id);
|
|
207
|
+
if (!target)
|
|
208
|
+
return void res.json({ error: '用户不存在' });
|
|
209
|
+
db.prepare("UPDATE users SET max_products = ?, updated_at = datetime('now') WHERE id = ?").run(n, req.params.id);
|
|
210
|
+
logAdminAction(admin.id, 'set_product_quota', 'user', req.params.id, { quota: n });
|
|
211
|
+
res.json({ success: true, max_products: n });
|
|
212
|
+
});
|
|
213
|
+
app.post('/api/admin/users/:id/pause-listing', (req, res) => {
|
|
214
|
+
const admin = requireUsersAdmin(req, res);
|
|
215
|
+
if (!admin)
|
|
216
|
+
return;
|
|
217
|
+
if (!adminCanOperateOn(admin, req.params.id, res))
|
|
218
|
+
return;
|
|
219
|
+
const { reason } = req.body;
|
|
220
|
+
if (!reason?.trim())
|
|
221
|
+
return void res.json({ error: '请填写暂停原因' });
|
|
222
|
+
db.prepare(`UPDATE users SET listing_paused = 1, listing_paused_reason = ?, listing_paused_by = ?, listing_paused_at = datetime('now'), updated_at = datetime('now') WHERE id = ?`)
|
|
223
|
+
.run(reason.trim(), admin.id, req.params.id);
|
|
224
|
+
logAdminAction(admin.id, 'pause_listing', 'user', req.params.id, { reason: reason.trim() });
|
|
225
|
+
res.json({ success: true });
|
|
226
|
+
});
|
|
227
|
+
app.post('/api/admin/users/:id/resume-listing', (req, res) => {
|
|
228
|
+
const admin = requireUsersAdmin(req, res);
|
|
229
|
+
if (!admin)
|
|
230
|
+
return;
|
|
231
|
+
if (!adminCanOperateOn(admin, req.params.id, res))
|
|
232
|
+
return;
|
|
233
|
+
db.prepare(`UPDATE users SET listing_paused = 0, listing_paused_reason = NULL, updated_at = datetime('now') WHERE id = ?`).run(req.params.id);
|
|
234
|
+
logAdminAction(admin.id, 'resume_listing', 'user', req.params.id, {});
|
|
235
|
+
res.json({ success: true });
|
|
236
|
+
});
|
|
237
|
+
}
|