@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.
Files changed (153) hide show
  1. package/LICENSE +48 -0
  2. package/README.md +156 -20
  3. package/dist/layer0-foundation/L0-1-database/schema.js +5 -4
  4. package/dist/layer0-foundation/L0-2-state-machine/engine.js +228 -7
  5. package/dist/layer0-foundation/L0-2-state-machine/order-chain.js +156 -0
  6. package/dist/layer0-foundation/L0-2-state-machine/transitions.js +53 -12
  7. package/dist/layer0-foundation/L0-5-manifest/manifest.js +14 -1
  8. package/dist/layer1-agent/L1-1-mcp-server/auth.js +1 -1
  9. package/dist/layer1-agent/L1-1-mcp-server/server.js +3691 -714
  10. package/dist/layer1-agent/L1-2-external-anchor/anchor-engine.js +324 -0
  11. package/dist/layer1-agent/L1-2-identity/agent-passport.js +100 -0
  12. package/dist/layer2-business/L2-6-notifications/notification-engine.js +72 -5
  13. package/dist/layer2-business/L2-7-snf/snf-engine.js +287 -0
  14. package/dist/layer2-business/L2-anchor-registry/anchor-registry.js +396 -0
  15. package/dist/layer2-business/L2-notes/note-photo-storage.js +133 -0
  16. package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +6 -6
  17. package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +246 -0
  18. package/dist/layer4-economics/L4-3-reputation/reputation-engine.js +95 -1
  19. package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +31 -2
  20. package/dist/layer4-economics/L4-4-skill-market/skill-listing-engine.js +358 -0
  21. package/dist/pwa/public/app.js +31947 -0
  22. package/dist/pwa/public/i18n.js +5751 -0
  23. package/dist/pwa/public/icon.svg +11 -0
  24. package/dist/pwa/public/index.html +21 -0
  25. package/dist/pwa/public/manifest.json +48 -0
  26. package/dist/pwa/public/openapi.json +5946 -0
  27. package/dist/pwa/public/style.css +535 -0
  28. package/dist/pwa/public/sw.js +63 -0
  29. package/dist/pwa/public/vendor/jsQR.js +10102 -0
  30. package/dist/pwa/public/webaz-logo.png +0 -0
  31. package/dist/pwa/routes/account-deletion.js +53 -0
  32. package/dist/pwa/routes/addresses.js +105 -0
  33. package/dist/pwa/routes/admin-admins.js +151 -0
  34. package/dist/pwa/routes/admin-analytics.js +253 -0
  35. package/dist/pwa/routes/admin-atomic.js +21 -0
  36. package/dist/pwa/routes/admin-catalog.js +64 -0
  37. package/dist/pwa/routes/admin-editor-picks.js +45 -0
  38. package/dist/pwa/routes/admin-events.js +60 -0
  39. package/dist/pwa/routes/admin-health.js +66 -0
  40. package/dist/pwa/routes/admin-moderation.js +120 -0
  41. package/dist/pwa/routes/admin-ops.js +179 -0
  42. package/dist/pwa/routes/admin-protocol-params.js +79 -0
  43. package/dist/pwa/routes/admin-reports.js +154 -0
  44. package/dist/pwa/routes/admin-tokenomics.js +113 -0
  45. package/dist/pwa/routes/admin-users-lifecycle.js +237 -0
  46. package/dist/pwa/routes/admin-users-query.js +390 -0
  47. package/dist/pwa/routes/admin-verifier-flow.js +126 -0
  48. package/dist/pwa/routes/admin-verifier-whitelist.js +111 -0
  49. package/dist/pwa/routes/admin-wallet-ops.js +66 -0
  50. package/dist/pwa/routes/agent-buy.js +215 -0
  51. package/dist/pwa/routes/agent-governance.js +341 -0
  52. package/dist/pwa/routes/agent-reputation.js +34 -0
  53. package/dist/pwa/routes/ai.js +101 -0
  54. package/dist/pwa/routes/analytics.js +272 -0
  55. package/dist/pwa/routes/anchors.js +169 -0
  56. package/dist/pwa/routes/announcements.js +110 -0
  57. package/dist/pwa/routes/arbitrator.js +117 -0
  58. package/dist/pwa/routes/auction.js +436 -0
  59. package/dist/pwa/routes/auth-login.js +40 -0
  60. package/dist/pwa/routes/auth-read.js +66 -0
  61. package/dist/pwa/routes/auth-register.js +138 -0
  62. package/dist/pwa/routes/auth-sessions.js +62 -0
  63. package/dist/pwa/routes/blocklist.js +60 -0
  64. package/dist/pwa/routes/buyer-feeds.js +224 -0
  65. package/dist/pwa/routes/cart.js +155 -0
  66. package/dist/pwa/routes/charity.js +816 -0
  67. package/dist/pwa/routes/chat.js +318 -0
  68. package/dist/pwa/routes/checkin-tasks.js +122 -0
  69. package/dist/pwa/routes/checkout-helpers.js +85 -0
  70. package/dist/pwa/routes/claim-initiators.js +88 -0
  71. package/dist/pwa/routes/claim-verify.js +615 -0
  72. package/dist/pwa/routes/claim-voting.js +114 -0
  73. package/dist/pwa/routes/claim-withdrawals.js +20 -0
  74. package/dist/pwa/routes/coupons.js +165 -0
  75. package/dist/pwa/routes/dashboards.js +99 -0
  76. package/dist/pwa/routes/dispute-cases.js +267 -0
  77. package/dist/pwa/routes/disputes-read.js +358 -0
  78. package/dist/pwa/routes/disputes-write.js +475 -0
  79. package/dist/pwa/routes/evidence.js +86 -0
  80. package/dist/pwa/routes/external-anchors.js +107 -0
  81. package/dist/pwa/routes/feedback.js +270 -0
  82. package/dist/pwa/routes/flash-sales.js +130 -0
  83. package/dist/pwa/routes/follows.js +103 -0
  84. package/dist/pwa/routes/group-buys.js +208 -0
  85. package/dist/pwa/routes/growth.js +199 -0
  86. package/dist/pwa/routes/import-product.js +153 -0
  87. package/dist/pwa/routes/kyc.js +40 -0
  88. package/dist/pwa/routes/leaderboard.js +149 -0
  89. package/dist/pwa/routes/listings.js +281 -0
  90. package/dist/pwa/routes/logistics.js +35 -0
  91. package/dist/pwa/routes/manifests.js +126 -0
  92. package/dist/pwa/routes/me-data.js +101 -0
  93. package/dist/pwa/routes/notifications.js +48 -0
  94. package/dist/pwa/routes/offers.js +96 -0
  95. package/dist/pwa/routes/orders-action.js +285 -0
  96. package/dist/pwa/routes/orders-create.js +339 -0
  97. package/dist/pwa/routes/orders-read.js +180 -0
  98. package/dist/pwa/routes/p2p-products.js +178 -0
  99. package/dist/pwa/routes/payments-governance.js +311 -0
  100. package/dist/pwa/routes/peers.js +34 -0
  101. package/dist/pwa/routes/pin-receipts.js +39 -0
  102. package/dist/pwa/routes/products-aliases.js +119 -0
  103. package/dist/pwa/routes/products-claims.js +60 -0
  104. package/dist/pwa/routes/products-create.js +206 -0
  105. package/dist/pwa/routes/products-crud.js +73 -0
  106. package/dist/pwa/routes/products-links.js +129 -0
  107. package/dist/pwa/routes/products-list.js +424 -0
  108. package/dist/pwa/routes/products-meta.js +155 -0
  109. package/dist/pwa/routes/products-update.js +125 -0
  110. package/dist/pwa/routes/profile-credentials.js +105 -0
  111. package/dist/pwa/routes/profile-identity.js +174 -0
  112. package/dist/pwa/routes/profile-location.js +35 -0
  113. package/dist/pwa/routes/profile-placement.js +70 -0
  114. package/dist/pwa/routes/profile-prefs.js +93 -0
  115. package/dist/pwa/routes/promoter.js +208 -0
  116. package/dist/pwa/routes/public-utils.js +170 -0
  117. package/dist/pwa/routes/push.js +54 -0
  118. package/dist/pwa/routes/ratings.js +220 -0
  119. package/dist/pwa/routes/recover-key.js +100 -0
  120. package/dist/pwa/routes/referral.js +58 -0
  121. package/dist/pwa/routes/reputation.js +34 -0
  122. package/dist/pwa/routes/returns.js +493 -0
  123. package/dist/pwa/routes/reviews.js +81 -0
  124. package/dist/pwa/routes/rfqs.js +443 -0
  125. package/dist/pwa/routes/search.js +172 -0
  126. package/dist/pwa/routes/secondhand.js +278 -0
  127. package/dist/pwa/routes/seller-quota.js +225 -0
  128. package/dist/pwa/routes/share-redirects.js +164 -0
  129. package/dist/pwa/routes/shareables-interactions.js +212 -0
  130. package/dist/pwa/routes/shareables.js +470 -0
  131. package/dist/pwa/routes/shops.js +98 -0
  132. package/dist/pwa/routes/signaling.js +43 -0
  133. package/dist/pwa/routes/skill-market.js +173 -0
  134. package/dist/pwa/routes/skills.js +174 -0
  135. package/dist/pwa/routes/snf.js +126 -0
  136. package/dist/pwa/routes/tags.js +47 -0
  137. package/dist/pwa/routes/trial.js +333 -0
  138. package/dist/pwa/routes/trusted-kpi.js +87 -0
  139. package/dist/pwa/routes/url-claim.js +113 -0
  140. package/dist/pwa/routes/users-public.js +317 -0
  141. package/dist/pwa/routes/variants.js +156 -0
  142. package/dist/pwa/routes/verifier-user.js +107 -0
  143. package/dist/pwa/routes/verify-tasks.js +120 -0
  144. package/dist/pwa/routes/waitlist.js +65 -0
  145. package/dist/pwa/routes/wallet-read.js +218 -0
  146. package/dist/pwa/routes/wallet-write.js +273 -0
  147. package/dist/pwa/routes/webauthn.js +188 -0
  148. package/dist/pwa/routes/webhooks.js +162 -0
  149. package/dist/pwa/routes/welcome.js +226 -0
  150. package/dist/pwa/routes/wishlist-qa.js +135 -0
  151. package/dist/pwa/security/ssrf.js +110 -0
  152. package/dist/pwa/server.js +9679 -698
  153. package/package.json +11 -4
@@ -0,0 +1,390 @@
1
+ export function registerAdminUsersQueryRoutes(app, deps) {
2
+ const { db, requireUsersAdmin, adminCanOperateOn, isRootAdmin, isAllowedSponsor, maskApiKey, computeLightTags, getAdminScope, getSellerDailyLimit, todayStartISO, broadcastSystemEvent, INTERNAL_AUDITOR_ID } = deps;
3
+ // P1-1: 按 handle / id 任意角色查找
4
+ app.get('/api/admin/users/lookup', (req, res) => {
5
+ const admin = requireUsersAdmin(req, res);
6
+ if (!admin)
7
+ return;
8
+ const raw = String(req.query.q || '').trim();
9
+ if (!raw)
10
+ return void res.status(400).json({ error: 'q 必填(user_id 或 handle)' });
11
+ const term = raw.replace(/^@/, '');
12
+ let user = db.prepare("SELECT id, name, handle, role, created_at FROM users WHERE handle = ? AND id NOT IN ('sys_protocol', ?)").get(term, INTERNAL_AUDITOR_ID);
13
+ if (!user)
14
+ user = db.prepare("SELECT id, name, handle, role, created_at FROM users WHERE id = ? AND id NOT IN ('sys_protocol', ?)").get(term, INTERNAL_AUDITOR_ID);
15
+ if (!user)
16
+ return void res.status(404).json({ error: '用户不存在' });
17
+ res.json({ user });
18
+ });
19
+ // Wave F-3: 完整事件流
20
+ app.get('/api/admin/users/:id/timeline', (req, res) => {
21
+ const admin = requireUsersAdmin(req, res);
22
+ if (!admin)
23
+ return;
24
+ const id = req.params.id;
25
+ const limit = Math.min(500, Math.max(10, Number(req.query.limit) || 100));
26
+ const events = [];
27
+ const push = (ts, type, icon, summary, refId, refType, amount) => {
28
+ if (!ts)
29
+ return;
30
+ events.push({ ts, type, icon, summary, ref_id: refId || null, ref_type: refType || null, amount: amount ?? null });
31
+ };
32
+ db.prepare(`SELECT id, status, total_amount, created_at, buyer_id, seller_id, logistics_id FROM orders WHERE buyer_id=? OR seller_id=? OR logistics_id=? ORDER BY created_at DESC LIMIT 100`).all(id, id, id).forEach(o => {
33
+ const role = o.buyer_id === id ? '买家' : o.seller_id === id ? '卖家' : '物流';
34
+ push(o.created_at, 'order', '📦', `订单 (${role}) ${o.id} · ${o.total_amount} WAZ · ${o.status}`, o.id, 'order', o.total_amount);
35
+ });
36
+ db.prepare(`SELECT order_id, stars, comment, created_at, buyer_id, seller_id FROM order_ratings WHERE buyer_id=? OR seller_id=? ORDER BY created_at DESC LIMIT 50`).all(id, id).forEach(r => {
37
+ const role = r.buyer_id === id ? '给出' : '收到';
38
+ push(r.created_at, 'rating', '⭐', `${role} ${r.stars} 星评价 (订单 ${r.order_id})`, r.order_id, 'order');
39
+ });
40
+ db.prepare(`SELECT id, order_id, reason, refund_amount, status, created_at, buyer_id, seller_id FROM return_requests WHERE buyer_id=? OR seller_id=? ORDER BY created_at DESC LIMIT 50`).all(id, id).forEach(r => {
41
+ const role = r.buyer_id === id ? '发起' : '收到';
42
+ push(r.created_at, 'return', '↩', `${role} 退货 (${r.status}, ${r.refund_amount} WAZ, ${r.reason})`, r.order_id, 'order', r.refund_amount);
43
+ });
44
+ db.prepare(`SELECT id, category, subject, status, created_at FROM feedback_tickets WHERE user_id=? ORDER BY created_at DESC LIMIT 30`).all(id).forEach(f => {
45
+ push(f.created_at, 'feedback', '💬', `反馈 (${f.category}/${f.status}): ${f.subject}`, f.id, 'feedback');
46
+ });
47
+ db.prepare(`SELECT checkin_date, reward, streak, created_at FROM daily_checkins WHERE user_id=? ORDER BY created_at DESC LIMIT 30`).all(id).forEach(c => {
48
+ push(c.created_at, 'checkin', '📅', `签到 ${c.checkin_date} · streak ${c.streak} · +${c.reward} WAZ`, null, null, c.reward);
49
+ });
50
+ db.prepare(`SELECT task_key, reward, claimed_at FROM task_completions WHERE user_id=? AND claimed_at IS NOT NULL ORDER BY claimed_at DESC LIMIT 30`).all(id).forEach(tc => {
51
+ push(tc.claimed_at, 'task', '🎁', `任务 ${tc.task_key} 领取 +${tc.reward} WAZ`, null, null, tc.reward);
52
+ });
53
+ db.prepare(`SELECT amount, source, ref, created_at FROM platform_reward_log WHERE user_id=? ORDER BY created_at DESC LIMIT 50`).all(id).forEach(p => {
54
+ push(p.created_at, 'reward', '💰', `平台拨付 (${p.source}${p.ref ? '/' + p.ref : ''}) +${p.amount} WAZ`, null, null, p.amount);
55
+ });
56
+ db.prepare(`SELECT followee_id, created_at FROM follows WHERE follower_id=? ORDER BY created_at DESC LIMIT 20`).all(id).forEach(f => {
57
+ push(f.created_at, 'follow', '🤝', `关注 ${f.followee_id}`, f.followee_id, 'user');
58
+ });
59
+ db.prepare(`SELECT product_id, created_at FROM user_wishlist WHERE user_id=? ORDER BY created_at DESC LIMIT 20`).all(id).forEach(w => {
60
+ push(w.created_at, 'wishlist', '❤', `加入心愿单 ${w.product_id}`, w.product_id, 'product');
61
+ });
62
+ db.prepare(`SELECT product_id, created_at FROM product_waitlist WHERE user_id=? ORDER BY created_at DESC LIMIT 20`).all(id).forEach(w => {
63
+ push(w.created_at, 'waitlist', '⏰', `加入补货提醒 ${w.product_id}`, w.product_id, 'product');
64
+ });
65
+ db.prepare(`SELECT id, status, ruling_type, created_at, resolved_at, initiator_id, defendant_id FROM disputes WHERE initiator_id=? OR defendant_id=? ORDER BY created_at DESC LIMIT 30`).all(id, id).forEach(d => {
66
+ const role = d.initiator_id === id ? '发起' : '被诉';
67
+ push(d.created_at, 'dispute_open', '⚖', `${role} 争议 ${d.id} (${d.status})`, d.id, 'dispute');
68
+ if (d.resolved_at)
69
+ push(d.resolved_at, 'dispute_resolved', '⚖', `争议 ${d.id} 结案 (${d.ruling_type || '—'})`, d.id, 'dispute');
70
+ });
71
+ const userRow = db.prepare(`SELECT created_at, name, role FROM users WHERE id=?`).get(id);
72
+ if (userRow)
73
+ push(userRow.created_at, 'register', '🎉', `注册账号 ${userRow.name} (${userRow.role})`, null, null);
74
+ events.sort((a, b) => String(b.ts).localeCompare(String(a.ts)));
75
+ res.json({ items: events.slice(0, limit), total: events.length });
76
+ });
77
+ app.post('/api/admin/users/batch-action', (req, res) => {
78
+ const admin = requireUsersAdmin(req, res);
79
+ if (!admin)
80
+ return;
81
+ const { user_ids, action, reason } = req.body || {};
82
+ if (!Array.isArray(user_ids) || user_ids.length === 0)
83
+ return void res.status(400).json({ error: 'user_ids 必填' });
84
+ if (user_ids.length > 200)
85
+ return void res.status(400).json({ error: '单次最多 200 用户' });
86
+ if (!['suspend', 'unsuspend'].includes(String(action)))
87
+ return void res.status(400).json({ error: 'action 必须 suspend/unsuspend' });
88
+ const reasonStr = action === 'suspend' ? (reason ? String(reason).slice(0, 200) : 'admin 批量暂停') : null;
89
+ const results = [];
90
+ for (const uid of user_ids) {
91
+ try {
92
+ if (uid === 'sys_protocol' || uid === admin.id) {
93
+ results.push({ user_id: uid, status: 'skipped', reason: '保留账户或自己' });
94
+ continue;
95
+ }
96
+ if (action === 'suspend') {
97
+ db.prepare(`INSERT INTO user_moderation (user_id, suspended, reason, suspended_by, suspended_at)
98
+ VALUES (?, 1, ?, ?, datetime('now'))
99
+ ON CONFLICT(user_id) DO UPDATE SET suspended = 1, reason = excluded.reason, suspended_by = excluded.suspended_by, suspended_at = excluded.suspended_at`)
100
+ .run(uid, reasonStr, admin.id);
101
+ }
102
+ else {
103
+ db.prepare(`UPDATE user_moderation SET suspended = 0 WHERE user_id = ?`).run(uid);
104
+ }
105
+ results.push({ user_id: uid, status: 'ok' });
106
+ }
107
+ catch (e) {
108
+ results.push({ user_id: uid, status: 'skipped', reason: e.message });
109
+ }
110
+ }
111
+ const ok = results.filter(r => r.status === 'ok').length;
112
+ try {
113
+ broadcastSystemEvent('admin_bulk_' + action, '🛡', `${admin.id} 批量${action === 'suspend' ? '暂停' : '解封'} ${ok} 用户`, null);
114
+ }
115
+ catch { }
116
+ res.json({ success: true, applied: ok, results });
117
+ });
118
+ app.get('/api/admin/users', (req, res) => {
119
+ const admin = requireUsersAdmin(req, res);
120
+ if (!admin)
121
+ return;
122
+ const q = req.query.q?.trim();
123
+ const role = req.query.role?.trim();
124
+ const type = req.query.type?.trim();
125
+ const region = req.query.region?.trim();
126
+ let sql = `SELECT u.id, u.name, u.role, u.roles, u.email, u.email_verified, u.created_at, u.failed_attempts,
127
+ u.region, u.admin_type, u.admin_scope,
128
+ COALESCE(m.suspended,0) as suspended, m.reason as suspend_reason,
129
+ vw.tier as v_tier, vw.is_system as v_is_system,
130
+ (SELECT 1 FROM verifier_applications va WHERE va.user_id = u.id AND va.status='pending' LIMIT 1) as v_app_pending
131
+ FROM users u
132
+ LEFT JOIN user_moderation m ON m.user_id = u.id
133
+ LEFT JOIN verifier_whitelist vw ON vw.user_id = u.id
134
+ WHERE u.id NOT IN ('sys_protocol', ?)`;
135
+ const params = [INTERNAL_AUDITOR_ID];
136
+ let match_mode = null;
137
+ if (q) {
138
+ if (q.startsWith('usr_')) {
139
+ sql += ` AND u.id = ?`;
140
+ params.push(q);
141
+ match_mode = 'id';
142
+ }
143
+ else if (q.includes('@')) {
144
+ sql += ` AND u.email = ?`;
145
+ params.push(q.toLowerCase());
146
+ match_mode = 'email';
147
+ }
148
+ else {
149
+ const qE = String(q).replace(/[\\%_]/g, '\\$&');
150
+ sql += ` AND u.name LIKE ? ESCAPE '\\'`;
151
+ params.push(`%${qE}%`);
152
+ match_mode = 'name';
153
+ }
154
+ }
155
+ if (role) {
156
+ sql += ` AND u.role = ?`;
157
+ params.push(role);
158
+ }
159
+ if (type === 'internal') {
160
+ sql += ` AND u.role IN ('admin','verifier','logistics','arbitrator')`;
161
+ }
162
+ else if (type === 'external') {
163
+ sql += ` AND u.role IN ('buyer','seller')`;
164
+ }
165
+ if (region) {
166
+ sql += ` AND u.region = ?`;
167
+ params.push(region);
168
+ }
169
+ // 区域 admin 仅看自己 scope 内的用户
170
+ if (!isRootAdmin(admin)) {
171
+ const scope = getAdminScope(admin);
172
+ if (scope !== 'global') {
173
+ sql += ` AND u.region = ?`;
174
+ params.push(scope);
175
+ }
176
+ }
177
+ sql += ` ORDER BY u.created_at DESC LIMIT 100`;
178
+ const rows = db.prepare(sql).all(...params);
179
+ res.json({
180
+ match_mode,
181
+ my_admin_type: admin.admin_type || 'root',
182
+ my_admin_scope: admin.admin_scope || 'global',
183
+ users: rows.map(r => {
184
+ const mod = { suspended: Number(r.suspended) };
185
+ const vw = r.v_tier ? { tier: r.v_tier, is_system: Number(r.v_is_system) } : null;
186
+ const vAppPending = !!r.v_app_pending;
187
+ return {
188
+ id: r.id,
189
+ name: r.name,
190
+ role: r.role,
191
+ roles: (() => { try {
192
+ return JSON.parse(r.roles || '[]');
193
+ }
194
+ catch {
195
+ return [];
196
+ } })(),
197
+ email: r.email,
198
+ email_verified: !!r.email_verified,
199
+ region: r.region,
200
+ admin_type: r.admin_type,
201
+ admin_scope: r.admin_scope,
202
+ created_at: r.created_at,
203
+ suspended: !!r.suspended,
204
+ suspend_reason: r.suspend_reason,
205
+ tags: computeLightTags(r, mod, vw, vAppPending),
206
+ };
207
+ }),
208
+ });
209
+ });
210
+ // 完整档案聚合
211
+ app.get('/api/admin/users/:id/profile', (req, res) => {
212
+ const admin = requireUsersAdmin(req, res);
213
+ if (!admin)
214
+ return;
215
+ if (!adminCanOperateOn(admin, req.params.id, res))
216
+ return;
217
+ const id = req.params.id;
218
+ const user = db.prepare("SELECT * FROM users WHERE id = ?").get(id);
219
+ if (!user)
220
+ return void res.json({ error: '用户不存在' });
221
+ const wallet = db.prepare("SELECT balance, staked, escrowed, earned, deposit_address FROM wallets WHERE user_id = ?").get(id);
222
+ const mod = db.prepare("SELECT suspended, reason, suspended_by, suspended_at FROM user_moderation WHERE user_id = ?").get(id);
223
+ const vw = db.prepare("SELECT tier, daily_quota, tasks_today, quota_reset_at, granted_by, stake_amount, cooldown_until, error_count_180d, is_system, added_at FROM verifier_whitelist WHERE user_id = ?").get(id);
224
+ const vs = db.prepare("SELECT verify_rights, tasks_done, tasks_correct, tasks_wrong, suspended_until FROM verifier_stats WHERE user_id = ?").get(id);
225
+ const vAppPending = !!db.prepare("SELECT 1 FROM verifier_applications WHERE user_id = ? AND status='pending' LIMIT 1").get(id);
226
+ const roleSet = new Set((() => { try {
227
+ return JSON.parse(user.roles || '[]');
228
+ }
229
+ catch {
230
+ return [];
231
+ } })());
232
+ const kpis = {};
233
+ if (roleSet.has('seller')) {
234
+ const p = db.prepare(`SELECT COUNT(*) as total,
235
+ SUM(CASE WHEN status='active' THEN 1 ELSE 0 END) as active,
236
+ SUM(CASE WHEN status='paused' THEN 1 ELSE 0 END) as paused,
237
+ SUM(CASE WHEN status='deleted'THEN 1 ELSE 0 END) as deleted
238
+ FROM products WHERE seller_id = ?`).get(id);
239
+ const o = db.prepare(`SELECT COUNT(*) as total,
240
+ SUM(CASE WHEN status='completed' THEN 1 ELSE 0 END) as completed,
241
+ COALESCE(SUM(CASE WHEN status='completed' THEN total_amount ELSE 0 END),0) as total_sales
242
+ FROM orders WHERE seller_id = ?`).get(id);
243
+ const d = db.prepare(`SELECT COUNT(*) as defendant_count,
244
+ SUM(CASE WHEN ruling_type IN ('refund_buyer','partial_refund') THEN 1 ELSE 0 END) as lost
245
+ FROM disputes WHERE defendant_id = ?`).get(id);
246
+ const today = todayStartISO();
247
+ const todayCount = db.prepare("SELECT COUNT(*) as n FROM products WHERE seller_id = ? AND created_at >= ?").get(id, today).n;
248
+ const dailyLimit = getSellerDailyLimit({ id, created_at: user.created_at });
249
+ kpis.seller = {
250
+ products_total: p.total, products_active: p.active, products_paused: p.paused, products_deleted: p.deleted,
251
+ orders_total: o.total, orders_completed: o.completed, total_sales: o.total_sales,
252
+ disputes_as_defendant: d.defendant_count, disputes_lost: d.lost,
253
+ max_products: Number(user.max_products ?? 200),
254
+ daily_limit: dailyLimit, daily_used: todayCount,
255
+ listing_paused: !!user.listing_paused,
256
+ listing_paused_reason: user.listing_paused_reason ?? null,
257
+ };
258
+ }
259
+ if (roleSet.has('buyer')) {
260
+ const o = db.prepare(`SELECT COUNT(*) as total,
261
+ SUM(CASE WHEN status='completed' THEN 1 ELSE 0 END) as completed,
262
+ COALESCE(SUM(CASE WHEN status='completed' THEN total_amount ELSE 0 END),0) as total_spent,
263
+ MAX(created_at) as last_order_at
264
+ FROM orders WHERE buyer_id = ?`).get(id);
265
+ kpis.buyer = {
266
+ orders_total: o.total, orders_completed: o.completed,
267
+ total_spent: o.total_spent, last_order_at: o.last_order_at,
268
+ };
269
+ }
270
+ if (roleSet.has('logistics')) {
271
+ const o = db.prepare(`SELECT COUNT(*) as total,
272
+ SUM(CASE WHEN status='completed' THEN 1 ELSE 0 END) as completed
273
+ FROM orders WHERE logistics_id = ?`).get(id);
274
+ kpis.logistics = { deliveries_total: o.total, deliveries_completed: o.completed };
275
+ }
276
+ if (roleSet.has('verifier') && vw) {
277
+ const accuracy = (vs && vs.tasks_done > 0)
278
+ ? Number(vs.tasks_correct / vs.tasks_done).toFixed(3) : '—';
279
+ kpis.verifier = {
280
+ tier: vw.tier, daily_quota: vw.daily_quota, tasks_today: vw.tasks_today,
281
+ remaining: Number(vw.daily_quota) > 0 ? Math.max(0, Number(vw.daily_quota) - Number(vw.tasks_today)) : 0,
282
+ tasks_done: vs?.tasks_done ?? 0, tasks_correct: vs?.tasks_correct ?? 0,
283
+ accuracy, error_count_180d: vw.error_count_180d,
284
+ verify_rights: vs?.verify_rights ?? 0,
285
+ suspended_until: vs?.suspended_until, cooldown_until: vw.cooldown_until,
286
+ is_system: vw.is_system === 1,
287
+ };
288
+ }
289
+ const tags = computeLightTags(user, mod ? { suspended: Number(mod.suspended) } : null, vw ? { tier: vw.tier, is_system: Number(vw.is_system) } : null, vAppPending);
290
+ if (wallet && Number(wallet.balance) > 1000)
291
+ tags.push('high_balance');
292
+ // 最近活动
293
+ const events = [];
294
+ const pushEvt = (ts, type, icon, summary, refId, refType) => {
295
+ if (!ts)
296
+ return;
297
+ events.push({ ts, type, icon, summary, ref_id: refId, ref_type: refType });
298
+ };
299
+ db.prepare(`SELECT id, total_amount, product_id, status, created_at FROM orders WHERE buyer_id = ? ORDER BY created_at DESC LIMIT 10`).all(id).forEach(o => pushEvt(o.created_at, 'order_buy', '🛒', `下单 ${o.total_amount} WAZ (${o.status})`, o.id, 'order'));
300
+ db.prepare(`SELECT id, total_amount, status, created_at FROM orders WHERE seller_id = ? ORDER BY created_at DESC LIMIT 10`).all(id).forEach(o => pushEvt(o.created_at, 'order_sell', '💰', `售出 ${o.total_amount} WAZ (${o.status})`, o.id, 'order'));
301
+ db.prepare(`SELECT id, title, status, created_at FROM products WHERE seller_id = ? ORDER BY created_at DESC LIMIT 10`).all(id).forEach(p => pushEvt(p.created_at, 'product_listed', '🏪', `上架商品 ${(p.title || '').slice(0, 30)}`, p.id, 'product'));
302
+ db.prepare(`SELECT id, task_id, verdict, claimed_at, submitted_at FROM verify_submissions WHERE verifier_id = ? ORDER BY claimed_at DESC LIMIT 10`).all(id).forEach(s => {
303
+ pushEvt(s.claimed_at, 'verify_claimed', '🔍', `认领验证任务`, s.task_id, 'task');
304
+ if (s.submitted_at) {
305
+ const icon = s.verdict === 'correct' ? '✓' : s.verdict === 'wrong' ? '✗' : '⏳';
306
+ pushEvt(s.submitted_at, 'verify_submitted', icon, `提交验证 (${s.verdict || 'pending'})`, s.task_id, 'task');
307
+ }
308
+ });
309
+ db.prepare(`SELECT id, status, ruling_type, created_at, resolved_at, initiator_id, defendant_id FROM disputes WHERE initiator_id = ? OR defendant_id = ? ORDER BY created_at DESC LIMIT 10`).all(id, id).forEach(d => {
310
+ const role = d.initiator_id === id ? '发起' : '被告';
311
+ pushEvt(d.created_at, 'dispute_init', '⚖', `争议 ${role} (${d.status})`, d.id, 'dispute');
312
+ if (d.resolved_at)
313
+ pushEvt(d.resolved_at, 'dispute_resolved', '⚖', `争议结案 (${d.ruling_type || '—'})`, d.id, 'dispute');
314
+ });
315
+ db.prepare(`SELECT id, status, decision_note, applied_at, reviewed_at FROM verifier_applications WHERE user_id = ? ORDER BY applied_at DESC LIMIT 5`).all(id).forEach(a => {
316
+ pushEvt(a.applied_at, 'verifier_apply', '📥', `提交审核员申请`, a.id, 'verifier_app');
317
+ if (a.reviewed_at)
318
+ pushEvt(a.reviewed_at, 'verifier_review', a.status === 'approved' ? '✅' : '❌', `申请${a.status === 'approved' ? '获批' : a.status === 'rejected' ? '被拒' : a.status}`, a.id, 'verifier_app');
319
+ });
320
+ db.prepare(`SELECT id, status, created_at, reviewed_at FROM verifier_appeals WHERE user_id = ? ORDER BY created_at DESC LIMIT 5`).all(id).forEach(a => {
321
+ pushEvt(a.created_at, 'appeal_submitted', '📩', `提交申诉`, a.id, 'appeal');
322
+ if (a.reviewed_at)
323
+ pushEvt(a.reviewed_at, 'appeal_decided', a.status === 'accepted' ? '✅' : '❌', `申诉${a.status === 'accepted' ? '成立' : '驳回'}`, a.id, 'appeal');
324
+ });
325
+ events.sort((a, b) => (a.ts < b.ts ? 1 : a.ts > b.ts ? -1 : 0));
326
+ const activity = events.slice(0, 20);
327
+ // 风险信号
328
+ const risks = [];
329
+ const failedAttempts = Number(user.failed_attempts ?? 0);
330
+ if (failedAttempts >= 3)
331
+ risks.push({ severity: 'medium', label: '近期多次登录失败', detail: `${failedAttempts} 次` });
332
+ if (user.locked_until && new Date(user.locked_until).getTime() > Date.now()) {
333
+ risks.push({ severity: 'high', label: '账户已锁定', detail: `解锁: ${user.locked_until}` });
334
+ }
335
+ const openDisputes = db.prepare("SELECT COUNT(*) as n FROM disputes WHERE defendant_id = ? AND status IN ('open','in_review')").get(id).n;
336
+ if (openDisputes > 0)
337
+ risks.push({ severity: 'medium', label: '未结争议作为被告', detail: `${openDisputes} 起` });
338
+ const lostCount = db.prepare("SELECT COUNT(*) as n FROM disputes WHERE defendant_id = ? AND ruling_type IN ('refund_buyer','partial_refund')").get(id).n;
339
+ if (lostCount > 0)
340
+ risks.push({ severity: 'low', label: '历史仲裁判输', detail: `${lostCount} 次` });
341
+ if (wallet && Number(wallet.staked) > Number(wallet.balance) * 2) {
342
+ risks.push({ severity: 'low', label: '钱包大额锁定', detail: `锁定 ${Number(wallet.staked).toFixed(2)} / 余 ${Number(wallet.balance).toFixed(2)}` });
343
+ }
344
+ if (vw && Number(vw.error_count_180d) >= 1 && !vw.is_system) {
345
+ const ec = Number(vw.error_count_180d);
346
+ risks.push({ severity: ec >= 2 ? 'high' : 'medium', label: '审核员近期错误', detail: `180 天 ${ec} 次` });
347
+ }
348
+ const audit = db.prepare(`
349
+ SELECT al.id, al.admin_id, al.action, al.detail, al.created_at,
350
+ u.name as admin_name
351
+ FROM admin_audit_log al
352
+ LEFT JOIN users u ON u.id = al.admin_id
353
+ WHERE al.target_type = 'user' AND al.target_id = ?
354
+ ORDER BY al.created_at DESC LIMIT 20
355
+ `).all(id).map(r => ({
356
+ ...r,
357
+ detail: r.detail ? (() => { try {
358
+ return JSON.parse(r.detail);
359
+ }
360
+ catch {
361
+ return r.detail;
362
+ } })() : null,
363
+ }));
364
+ res.json({
365
+ basic: {
366
+ id: user.id, name: user.name,
367
+ role: user.role,
368
+ roles: Array.from(roleSet),
369
+ api_key_masked: maskApiKey(user.api_key),
370
+ created_at: user.created_at, updated_at: user.updated_at,
371
+ email: user.email, email_verified: !!user.email_verified,
372
+ phone: user.phone, phone_verified: !!user.phone_verified,
373
+ has_password: !!user.password_hash,
374
+ reputation: user.reputation,
375
+ failed_attempts: user.failed_attempts ?? 0,
376
+ locked_until: user.locked_until,
377
+ mgmt_bonus_eligible: !!user.mgmt_bonus_eligible,
378
+ l1_share_override: Number(user.l1_share_override ?? 0),
379
+ can_l1_share: isAllowedSponsor(user.id),
380
+ },
381
+ wallet: wallet ?? null,
382
+ moderation: mod ?? null,
383
+ tags,
384
+ kpis,
385
+ activity,
386
+ risks,
387
+ audit,
388
+ });
389
+ });
390
+ }
@@ -0,0 +1,126 @@
1
+ export function registerAdminVerifierFlowRoutes(app, deps) {
2
+ const { db, requireVerifierMgmtAdmin, TIER_QUOTAS, VERIFIER_STAKE_REQUIRED, todayStartISO, logAdminAction } = deps;
3
+ app.get('/api/admin/verifier-applications', (req, res) => {
4
+ const admin = requireVerifierMgmtAdmin(req, res);
5
+ if (!admin)
6
+ return;
7
+ const status = req.query.status || 'pending';
8
+ const rows = db.prepare(`
9
+ SELECT va.id, va.user_id, va.status, va.applied_at, va.reviewed_at, va.reviewed_by, va.decision_note, va.snapshot,
10
+ u.name as user_name, u.email
11
+ FROM verifier_applications va
12
+ LEFT JOIN users u ON u.id = va.user_id
13
+ WHERE va.status = ?
14
+ ORDER BY va.applied_at DESC LIMIT 100
15
+ `).all(status);
16
+ res.json({
17
+ applications: rows.map(r => ({
18
+ ...r,
19
+ snapshot: r.snapshot ? (() => { try {
20
+ return JSON.parse(r.snapshot);
21
+ }
22
+ catch {
23
+ return r.snapshot;
24
+ } })() : null,
25
+ })),
26
+ });
27
+ });
28
+ app.post('/api/admin/verifier-applications/:id/approve', (req, res) => {
29
+ const admin = requireVerifierMgmtAdmin(req, res);
30
+ if (!admin)
31
+ return;
32
+ const { tier, note } = req.body;
33
+ const validTier = ['trial-1', 'trial-2', 'trial-3', 'active-1', 'active-2'].includes(tier) ? tier : 'trial-1';
34
+ const apl = db.prepare("SELECT id, user_id, status FROM verifier_applications WHERE id = ?").get(req.params.id);
35
+ if (!apl)
36
+ return void res.json({ error: '申请不存在' });
37
+ if (apl.status !== 'pending')
38
+ return void res.json({ error: '该申请不在待审状态' });
39
+ db.prepare("UPDATE verifier_applications SET status='approved', reviewed_at=datetime('now'), reviewed_by=?, decision_note=? WHERE id=?")
40
+ .run(admin.id, note || null, apl.id);
41
+ db.prepare(`INSERT OR REPLACE INTO verifier_whitelist
42
+ (user_id, note, tier, daily_quota, tasks_today, quota_reset_at, granted_by, stake_amount, is_system)
43
+ VALUES (?,?,?,?,0,?,?,?,0)`)
44
+ .run(apl.user_id, note || `批准为 ${validTier}`, validTier, TIER_QUOTAS[validTier], todayStartISO(), admin.id, VERIFIER_STAKE_REQUIRED);
45
+ db.prepare("INSERT OR IGNORE INTO verifier_stats (user_id) VALUES (?)").run(apl.user_id);
46
+ logAdminAction(admin.id, 'approve_verifier', 'user', apl.user_id, { tier: validTier, note });
47
+ res.json({ success: true });
48
+ });
49
+ app.post('/api/admin/verifier-applications/:id/reject', (req, res) => {
50
+ const admin = requireVerifierMgmtAdmin(req, res);
51
+ if (!admin)
52
+ return;
53
+ const { note } = req.body;
54
+ const apl = db.prepare("SELECT id, user_id, status FROM verifier_applications WHERE id = ?").get(req.params.id);
55
+ if (!apl)
56
+ return void res.json({ error: '申请不存在' });
57
+ if (apl.status !== 'pending')
58
+ return void res.json({ error: '该申请不在待审状态' });
59
+ db.prepare("UPDATE verifier_applications SET status='rejected', reviewed_at=datetime('now'), reviewed_by=?, decision_note=? WHERE id=?")
60
+ .run(admin.id, note || null, apl.id);
61
+ // 退回质押
62
+ if (VERIFIER_STAKE_REQUIRED > 0) {
63
+ db.prepare("UPDATE wallets SET balance = balance + ?, staked = staked - ? WHERE user_id = ?")
64
+ .run(VERIFIER_STAKE_REQUIRED, VERIFIER_STAKE_REQUIRED, apl.user_id);
65
+ }
66
+ logAdminAction(admin.id, 'reject_verifier', 'user', apl.user_id, { note });
67
+ res.json({ success: true });
68
+ });
69
+ app.get('/api/admin/verifier-appeals', (req, res) => {
70
+ const admin = requireVerifierMgmtAdmin(req, res);
71
+ if (!admin)
72
+ return;
73
+ const status = req.query.status || 'pending';
74
+ const rows = db.prepare(`
75
+ SELECT va.id, va.user_id, va.task_id, va.submission_id, va.reason, va.evidence_urls, va.status,
76
+ va.admin_note, va.reviewed_by, va.reviewed_at, va.created_at, u.name as user_name
77
+ FROM verifier_appeals va LEFT JOIN users u ON u.id = va.user_id
78
+ WHERE va.status = ?
79
+ ORDER BY va.created_at DESC LIMIT 100
80
+ `).all(status);
81
+ res.json({
82
+ appeals: rows.map(r => ({
83
+ ...r,
84
+ evidence_urls: r.evidence_urls ? (() => { try {
85
+ return JSON.parse(r.evidence_urls);
86
+ }
87
+ catch {
88
+ return [];
89
+ } })() : [],
90
+ })),
91
+ });
92
+ });
93
+ app.post('/api/admin/verifier-appeals/:id/decide', (req, res) => {
94
+ const admin = requireVerifierMgmtAdmin(req, res);
95
+ if (!admin)
96
+ return;
97
+ const { decision, note } = req.body; // 'accepted' | 'rejected'
98
+ if (!['accepted', 'rejected'].includes(decision))
99
+ return void res.json({ error: 'decision 无效' });
100
+ const appeal = db.prepare("SELECT id, user_id, status FROM verifier_appeals WHERE id = ?").get(req.params.id);
101
+ if (!appeal)
102
+ return void res.json({ error: '申诉不存在' });
103
+ if (appeal.status !== 'pending')
104
+ return void res.json({ error: '该申诉已处理' });
105
+ db.prepare("UPDATE verifier_appeals SET status = ?, admin_note = ?, reviewed_by = ?, reviewed_at = datetime('now') WHERE id = ?")
106
+ .run(decision, note || null, admin.id, appeal.id);
107
+ if (decision === 'accepted') {
108
+ // 解封 + 验证权 +2 + 错误次数 -1
109
+ db.prepare("UPDATE verifier_stats SET suspended_until = NULL, verify_rights = verify_rights + 2 WHERE user_id = ?").run(appeal.user_id);
110
+ db.prepare("UPDATE verifier_whitelist SET error_count_180d = MAX(0, error_count_180d - 1) WHERE user_id = ?").run(appeal.user_id);
111
+ // 完整重审:翻转该 verifier 在该 task 的 verdict + 补发奖励 + 翻回 stats
112
+ const fullAppeal = db.prepare("SELECT task_id FROM verifier_appeals WHERE id = ?").get(appeal.id);
113
+ if (fullAppeal?.task_id) {
114
+ const sub = db.prepare("SELECT vs.id, vs.verdict, vt.reward_per_verifier FROM verify_submissions vs JOIN verify_tasks vt ON vt.id = vs.task_id WHERE vs.task_id = ? AND vs.verifier_id = ?")
115
+ .get(fullAppeal.task_id, appeal.user_id);
116
+ if (sub && sub.verdict === 'wrong') {
117
+ db.prepare("UPDATE verify_submissions SET verdict = 'correct' WHERE id = ?").run(sub.id);
118
+ db.prepare("UPDATE verifier_stats SET tasks_correct = tasks_correct + 1, tasks_wrong = MAX(0, tasks_wrong - 1), verify_rights = verify_rights + 3 WHERE user_id = ?").run(appeal.user_id);
119
+ db.prepare("UPDATE wallets SET balance = balance + ? WHERE user_id = ?").run(sub.reward_per_verifier, appeal.user_id);
120
+ }
121
+ }
122
+ }
123
+ logAdminAction(admin.id, 'decide_appeal', 'user', appeal.user_id, { decision, note: note || null });
124
+ res.json({ success: true });
125
+ });
126
+ }
@@ -0,0 +1,111 @@
1
+ export function registerAdminVerifierWhitelistRoutes(app, deps) {
2
+ const { db, requireVerifierMgmtAdmin, adminCanOperateOn, logAdminAction, INTERNAL_AUDITOR_ID, TIER_QUOTAS, REVOKE_COOLDOWN_DAYS } = deps;
3
+ app.get('/api/admin/verifier-whitelist', (req, res) => {
4
+ const user = requireVerifierMgmtAdmin(req, res);
5
+ if (!user)
6
+ return;
7
+ const list = db.prepare(`
8
+ SELECT vw.user_id, vw.added_at, vw.note, u.name, u.role
9
+ FROM verifier_whitelist vw
10
+ JOIN users u ON u.id = vw.user_id
11
+ ORDER BY vw.added_at ASC
12
+ `).all();
13
+ res.json(list);
14
+ });
15
+ app.post('/api/admin/verifier-whitelist', (req, res) => {
16
+ const admin = requireVerifierMgmtAdmin(req, res);
17
+ if (!admin)
18
+ return;
19
+ const { user_id, name, note } = req.body;
20
+ let targetId = user_id;
21
+ if (!targetId && name) {
22
+ const found = db.prepare('SELECT id FROM users WHERE name = ?').get(name);
23
+ if (!found)
24
+ return void res.json({ error: `用户「${name}」不存在` });
25
+ targetId = found.id;
26
+ }
27
+ if (!targetId)
28
+ return void res.json({ error: '请提供 user_id 或 name' });
29
+ const target = db.prepare('SELECT id, name FROM users WHERE id = ?').get(targetId);
30
+ if (!target)
31
+ return void res.json({ error: '用户不存在' });
32
+ if (!adminCanOperateOn(admin, targetId, res))
33
+ return;
34
+ db.prepare('INSERT OR IGNORE INTO verifier_whitelist (user_id, note) VALUES (?, ?)').run(targetId, note ?? null);
35
+ res.json({ success: true, user_id: targetId, name: target.name });
36
+ });
37
+ app.delete('/api/admin/verifier-whitelist/:userId', (req, res) => {
38
+ const admin = requireVerifierMgmtAdmin(req, res);
39
+ if (!admin)
40
+ return;
41
+ const targetId = String(req.params.userId);
42
+ if (targetId === INTERNAL_AUDITOR_ID)
43
+ return void res.json({ error: '内部审核员不可移除' });
44
+ if (!adminCanOperateOn(admin, targetId, res))
45
+ return;
46
+ db.prepare('DELETE FROM verifier_whitelist WHERE user_id = ?').run(targetId);
47
+ res.json({ success: true });
48
+ });
49
+ app.post('/api/admin/verifier-whitelist/:userId/promote', (req, res) => {
50
+ const admin = requireVerifierMgmtAdmin(req, res);
51
+ if (!admin)
52
+ return;
53
+ if (!adminCanOperateOn(admin, req.params.userId, res))
54
+ return;
55
+ const { tier } = req.body;
56
+ if (!TIER_QUOTAS[tier])
57
+ return void res.json({ error: 'tier 无效' });
58
+ const targetId = req.params.userId;
59
+ const wl = db.prepare("SELECT is_system FROM verifier_whitelist WHERE user_id = ?").get(targetId);
60
+ if (!wl)
61
+ return void res.json({ error: '该用户不在白名单' });
62
+ if (wl.is_system)
63
+ return void res.json({ error: '系统兜底账户不可手动 promote' });
64
+ db.prepare("UPDATE verifier_whitelist SET tier = ?, daily_quota = ? WHERE user_id = ?")
65
+ .run(tier, TIER_QUOTAS[tier], targetId);
66
+ logAdminAction(admin.id, 'promote_verifier', 'user', targetId, { tier });
67
+ res.json({ success: true });
68
+ });
69
+ app.post('/api/admin/verifier-whitelist/:userId/suspend', (req, res) => {
70
+ const admin = requireVerifierMgmtAdmin(req, res);
71
+ if (!admin)
72
+ return;
73
+ if (!adminCanOperateOn(admin, req.params.userId, res))
74
+ return;
75
+ const { days, reason } = req.body;
76
+ const targetId = req.params.userId;
77
+ const n = Number(days) > 0 ? Number(days) : 7;
78
+ const until = new Date(Date.now() + n * 86400_000).toISOString();
79
+ db.prepare("INSERT OR IGNORE INTO verifier_stats (user_id) VALUES (?)").run(targetId);
80
+ db.prepare("UPDATE verifier_stats SET suspended_until = ? WHERE user_id = ?").run(until, targetId);
81
+ logAdminAction(admin.id, 'suspend_verifier', 'user', targetId, { days: n, reason: reason || null, until });
82
+ res.json({ success: true, suspended_until: until });
83
+ });
84
+ app.post('/api/admin/verifier-whitelist/:userId/revoke', (req, res) => {
85
+ const admin = requireVerifierMgmtAdmin(req, res);
86
+ if (!admin)
87
+ return;
88
+ if (!adminCanOperateOn(admin, req.params.userId, res))
89
+ return;
90
+ const { reason } = req.body;
91
+ const targetId = req.params.userId;
92
+ const wl = db.prepare("SELECT is_system, stake_amount FROM verifier_whitelist WHERE user_id = ?").get(targetId);
93
+ if (!wl)
94
+ return void res.json({ error: '该用户不在白名单' });
95
+ if (wl.is_system)
96
+ return void res.json({ error: '系统兜底账户不可撤销' });
97
+ const cooldownUntil = new Date(Date.now() + REVOKE_COOLDOWN_DAYS * 86400_000).toISOString();
98
+ // 没收 50% 质押
99
+ const forfeit = (wl.stake_amount || 0) * 0.5;
100
+ db.prepare("UPDATE wallets SET staked = staked - ? WHERE user_id = ?").run(wl.stake_amount || 0, targetId);
101
+ if ((wl.stake_amount || 0) > forfeit) {
102
+ db.prepare("UPDATE wallets SET balance = balance + ? WHERE user_id = ?").run((wl.stake_amount || 0) - forfeit, targetId);
103
+ }
104
+ db.prepare("DELETE FROM verifier_whitelist WHERE user_id = ?").run(targetId);
105
+ // 用 cooldown 记录在 verifier_stats 上(应用层兼容)
106
+ db.prepare(`INSERT INTO verifier_whitelist (user_id, note, tier, daily_quota, cooldown_until, is_system) VALUES (?,?,?,?,?,0)`)
107
+ .run(targetId, `撤销冷却中: ${reason || ''}`, 'trial-1', 0, cooldownUntil);
108
+ logAdminAction(admin.id, 'revoke_verifier', 'user', targetId, { reason: reason || null, forfeit, cooldown_until: cooldownUntil });
109
+ res.json({ success: true, cooldown_until: cooldownUntil, forfeit });
110
+ });
111
+ }