@seasonkoh/webaz 0.1.24 → 0.1.26
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 +5 -1
- package/dist/layer0-foundation/L0-1-database/db-backends/pg-backend.js +51 -0
- package/dist/layer0-foundation/L0-1-database/db-backends/sql-dialect-datetime.js +437 -0
- package/dist/layer0-foundation/L0-1-database/db-backends/sql-placeholders.js +98 -0
- package/dist/layer0-foundation/L0-1-database/db.js +65 -0
- package/dist/layer0-foundation/L0-2-state-machine/order-chain.js +13 -11
- package/dist/layer0-foundation/L0-2-state-machine/transitions.js +1 -1
- package/dist/layer0-foundation/L0-5-manifest/manifest.js +13 -11
- package/dist/layer1-agent/L1-1-mcp-server/server.js +288 -208
- package/dist/layer1-agent/L1-2-external-anchor/anchor-engine.js +14 -12
- package/dist/layer2-business/L2-6-notifications/notification-engine.js +8 -5
- package/dist/layer2-business/L2-7-snf/snf-engine.js +16 -14
- package/dist/layer2-business/L2-8-feedback/build-feedback-engine.js +18 -10
- package/dist/layer2-business/L2-9-contribution/build-reputation-engine.js +37 -23
- package/dist/layer2-business/L2-9-contribution/build-task-agent-metadata-store.js +182 -0
- package/dist/layer2-business/L2-9-contribution/build-task-participation.js +47 -0
- package/dist/layer2-business/L2-9-contribution/build-task-read.js +222 -0
- package/dist/layer2-business/L2-9-contribution/build-tasks-engine.js +11 -3
- package/dist/layer2-business/L2-9-contribution/canonical-contribution-target.js +16 -0
- package/dist/layer2-business/L2-9-contribution/contribution-display-envelope.js +40 -0
- package/dist/layer2-business/L2-9-contribution/contribution-score-contract.js +36 -0
- package/dist/layer2-business/L2-9-contribution/contribution-score-evidence.js +61 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/canonical.js +60 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/github-credential.schema.js +140 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/github-fetch-adapter.js +437 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/self-consistency.js +38 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/verifier.js +231 -0
- package/dist/layer2-business/L2-9-contribution/github-credential-ingestion-engine.js +145 -0
- package/dist/layer2-business/L2-9-contribution/github-credential-store.js +115 -0
- package/dist/layer2-business/L2-9-contribution/identity-binding-engine.js +134 -0
- package/dist/layer2-business/L2-9-contribution/identity-binding-store.js +101 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-challenge-engine.js +126 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-challenge-store.js +30 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-discovery.js +55 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-engine.js +109 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-fact-precondition.js +22 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-proof-verifier.js +97 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-read.js +59 -0
- package/dist/layer2-business/L2-9-contribution/task-proposal-ai-store.js +99 -0
- package/dist/layer2-business/L2-9-contribution/task-proposal-draft.js +191 -0
- package/dist/layer2-business/L2-9-contribution/task-proposal-store.js +129 -0
- package/dist/layer2-business/L2-notes/note-photo-storage.js +4 -2
- package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +17 -15
- package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +11 -8
- package/dist/layer4-economics/L4-3-reputation/reputation-engine.js +9 -8
- package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +11 -8
- package/dist/layer4-economics/L4-4-skill-market/skill-listing-engine.js +22 -16
- package/dist/pwa/acp-feed.js +13 -1
- package/dist/pwa/admin-bearer-auth.js +21 -0
- package/dist/pwa/contract-fingerprint.js +2 -0
- package/dist/pwa/email-delivery.js +127 -0
- package/dist/pwa/endpoint-actions.js +5 -1
- package/dist/pwa/goal-index.js +8 -8
- package/dist/pwa/human-presence.js +62 -0
- package/dist/pwa/public/app.js +1485 -283
- package/dist/pwa/public/i18n.js +297 -59
- package/dist/pwa/public/index.html +1 -0
- package/dist/pwa/public/openapi.json +5 -5
- package/dist/pwa/public/whitepaper/en/index.html +153 -0
- package/dist/pwa/public/whitepaper/zh-CN/index.html +153 -0
- package/dist/pwa/rate-limit.js +22 -0
- package/dist/pwa/routes/account-deletion.js +15 -13
- package/dist/pwa/routes/addresses.js +10 -9
- package/dist/pwa/routes/admin-admins.js +13 -14
- package/dist/pwa/routes/admin-analytics.js +109 -69
- package/dist/pwa/routes/admin-atomic.js +10 -4
- package/dist/pwa/routes/admin-catalog.js +13 -11
- package/dist/pwa/routes/admin-editor-picks.js +15 -10
- package/dist/pwa/routes/admin-events.js +5 -3
- package/dist/pwa/routes/admin-health.js +2 -1
- package/dist/pwa/routes/admin-moderation.js +50 -29
- package/dist/pwa/routes/admin-ops.js +35 -23
- package/dist/pwa/routes/admin-protocol-params.js +16 -19
- package/dist/pwa/routes/admin-reports.js +23 -21
- package/dist/pwa/routes/admin-tokenomics.js +26 -25
- package/dist/pwa/routes/admin-users-lifecycle.js +37 -40
- package/dist/pwa/routes/admin-users-query.js +65 -53
- package/dist/pwa/routes/admin-verifier-flow.js +82 -41
- package/dist/pwa/routes/admin-verifier-whitelist.js +55 -27
- package/dist/pwa/routes/admin-wallet-ops.js +32 -7
- package/dist/pwa/routes/agent-buy.js +46 -22
- package/dist/pwa/routes/agent-governance.js +52 -56
- package/dist/pwa/routes/ai.js +7 -5
- package/dist/pwa/routes/analytics.js +43 -41
- package/dist/pwa/routes/anchors.js +19 -20
- package/dist/pwa/routes/announcements.js +13 -13
- package/dist/pwa/routes/arbitrator.js +97 -31
- package/dist/pwa/routes/auction.js +157 -116
- package/dist/pwa/routes/auth-login.js +6 -4
- package/dist/pwa/routes/auth-read.js +21 -10
- package/dist/pwa/routes/auth-register.js +111 -26
- package/dist/pwa/routes/auth-sessions.js +12 -11
- package/dist/pwa/routes/blocklist.js +16 -15
- package/dist/pwa/routes/build-feedback.js +10 -9
- package/dist/pwa/routes/build-reputation.js +6 -2
- package/dist/pwa/routes/build-tasks.js +45 -13
- package/dist/pwa/routes/buyer-feeds.js +27 -25
- package/dist/pwa/routes/cart.js +16 -15
- package/dist/pwa/routes/charity.js +212 -150
- package/dist/pwa/routes/chat.js +42 -43
- package/dist/pwa/routes/checkin-tasks.js +10 -9
- package/dist/pwa/routes/checkout-helpers.js +12 -10
- package/dist/pwa/routes/claim-initiators.js +34 -14
- package/dist/pwa/routes/claim-verify.js +86 -53
- package/dist/pwa/routes/claim-voting.js +43 -18
- package/dist/pwa/routes/contribution-identity.js +164 -0
- package/dist/pwa/routes/contribution-score.js +19 -0
- package/dist/pwa/routes/coupons.js +19 -16
- package/dist/pwa/routes/dashboards.js +18 -16
- package/dist/pwa/routes/dispute-cases.js +25 -24
- package/dist/pwa/routes/disputes-read.js +45 -51
- package/dist/pwa/routes/disputes-write.js +124 -61
- package/dist/pwa/routes/evidence.js +9 -9
- package/dist/pwa/routes/external-anchors.js +13 -12
- package/dist/pwa/routes/feedback.js +29 -33
- package/dist/pwa/routes/flash-sales.js +18 -16
- package/dist/pwa/routes/follows.js +25 -24
- package/dist/pwa/routes/governance-auto-deactivate.js +21 -9
- package/dist/pwa/routes/governance-onboarding.js +70 -59
- package/dist/pwa/routes/group-buys.js +22 -22
- package/dist/pwa/routes/growth.js +34 -31
- package/dist/pwa/routes/import-product.js +12 -10
- package/dist/pwa/routes/kyc.js +9 -8
- package/dist/pwa/routes/leaderboard.js +20 -18
- package/dist/pwa/routes/listings.js +23 -22
- package/dist/pwa/routes/logistics.js +10 -8
- package/dist/pwa/routes/manifests.js +27 -27
- package/dist/pwa/routes/me-data.js +23 -21
- package/dist/pwa/routes/notifications.js +7 -6
- package/dist/pwa/routes/offers.js +30 -12
- package/dist/pwa/routes/orders-action.js +51 -29
- package/dist/pwa/routes/orders-create.js +75 -20
- package/dist/pwa/routes/orders-read.js +21 -20
- package/dist/pwa/routes/p2p-products.js +30 -18
- package/dist/pwa/routes/payments-governance.js +61 -56
- package/dist/pwa/routes/peers.js +9 -8
- package/dist/pwa/routes/pin-receipts.js +13 -13
- package/dist/pwa/routes/products-aliases.js +12 -10
- package/dist/pwa/routes/products-claims.js +36 -17
- package/dist/pwa/routes/products-create.js +53 -38
- package/dist/pwa/routes/products-crud.js +17 -16
- package/dist/pwa/routes/products-links.js +49 -26
- package/dist/pwa/routes/products-list.js +6 -4
- package/dist/pwa/routes/products-meta.js +40 -39
- package/dist/pwa/routes/products-update.js +19 -5
- package/dist/pwa/routes/profile-credentials.js +20 -19
- package/dist/pwa/routes/profile-identity.js +14 -13
- package/dist/pwa/routes/profile-location.js +7 -6
- package/dist/pwa/routes/profile-placement.js +20 -19
- package/dist/pwa/routes/profile-prefs.js +11 -11
- package/dist/pwa/routes/promoter.js +58 -66
- package/dist/pwa/routes/public-build-tasks.js +19 -0
- package/dist/pwa/routes/public-utils.js +108 -46
- package/dist/pwa/routes/push.js +16 -15
- package/dist/pwa/routes/ratings.js +92 -32
- package/dist/pwa/routes/recover-key.js +66 -26
- package/dist/pwa/routes/referral.js +37 -52
- package/dist/pwa/routes/reputation.js +3 -2
- package/dist/pwa/routes/returns.js +76 -73
- package/dist/pwa/routes/reviews.js +41 -18
- package/dist/pwa/routes/rewards-apply.js +16 -15
- package/dist/pwa/routes/rewards-auto-downgrade.js +9 -7
- package/dist/pwa/routes/rewards-escrow-expire.js +7 -5
- package/dist/pwa/routes/rfqs.js +163 -85
- package/dist/pwa/routes/search.js +16 -14
- package/dist/pwa/routes/secondhand.js +25 -22
- package/dist/pwa/routes/seller-quota.js +24 -26
- package/dist/pwa/routes/share-redirects.js +60 -55
- package/dist/pwa/routes/shareables-interactions.js +34 -35
- package/dist/pwa/routes/shareables.js +55 -51
- package/dist/pwa/routes/shop-referral.js +58 -0
- package/dist/pwa/routes/shops.js +25 -20
- package/dist/pwa/routes/signaling.js +10 -9
- package/dist/pwa/routes/skill-market.js +16 -16
- package/dist/pwa/routes/skills.js +15 -14
- package/dist/pwa/routes/snf.js +14 -13
- package/dist/pwa/routes/tags.js +10 -9
- package/dist/pwa/routes/task-proposals.js +121 -0
- package/dist/pwa/routes/trial.js +72 -52
- package/dist/pwa/routes/trusted-kpi.js +20 -18
- package/dist/pwa/routes/url-claim.js +67 -28
- package/dist/pwa/routes/users-public.js +62 -70
- package/dist/pwa/routes/variants.js +12 -13
- package/dist/pwa/routes/verifier-user.js +61 -21
- package/dist/pwa/routes/verify-tasks.js +49 -25
- package/dist/pwa/routes/waitlist.js +16 -15
- package/dist/pwa/routes/wallet-read.js +75 -37
- package/dist/pwa/routes/wallet-write.js +12 -9
- package/dist/pwa/routes/webauthn.js +25 -26
- package/dist/pwa/routes/webhooks.js +26 -26
- package/dist/pwa/routes/welcome.js +45 -50
- package/dist/pwa/routes/wishlist-qa.js +29 -32
- package/dist/pwa/server.js +304 -90
- package/dist/version.js +1 -1
- package/package.json +76 -3
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
export function registerAdminUsersQueryRoutes(app, deps) {
|
|
2
|
-
|
|
3
|
+
// db 已全量走 RFC-016 异步 seam(dbOne/dbAll/dbRun),不再直接用 deps.db
|
|
4
|
+
const { requireUsersAdmin, adminCanOperateOn, isRootAdmin, isAllowedSponsor, maskApiKey, computeLightTags, getAdminScope, getSellerDailyLimit, todayStartISO, broadcastSystemEvent, INTERNAL_AUDITOR_ID, logAdminAction } = deps;
|
|
3
5
|
// P1-1: 按 handle / id 任意角色查找
|
|
4
|
-
app.get('/api/admin/users/lookup', (req, res) => {
|
|
6
|
+
app.get('/api/admin/users/lookup', async (req, res) => {
|
|
5
7
|
const admin = requireUsersAdmin(req, res);
|
|
6
8
|
if (!admin)
|
|
7
9
|
return;
|
|
@@ -9,15 +11,15 @@ export function registerAdminUsersQueryRoutes(app, deps) {
|
|
|
9
11
|
if (!raw)
|
|
10
12
|
return void res.status(400).json({ error: 'q 必填(user_id 或 handle)' });
|
|
11
13
|
const term = raw.replace(/^@/, '');
|
|
12
|
-
let user =
|
|
14
|
+
let user = await dbOne("SELECT id, name, handle, role, created_at FROM users WHERE handle = ? AND id NOT IN ('sys_protocol', ?)", [term, INTERNAL_AUDITOR_ID]);
|
|
13
15
|
if (!user)
|
|
14
|
-
user =
|
|
16
|
+
user = await dbOne("SELECT id, name, handle, role, created_at FROM users WHERE id = ? AND id NOT IN ('sys_protocol', ?)", [term, INTERNAL_AUDITOR_ID]);
|
|
15
17
|
if (!user)
|
|
16
18
|
return void res.status(404).json({ error: '用户不存在' });
|
|
17
19
|
res.json({ user });
|
|
18
20
|
});
|
|
19
21
|
// Wave F-3: 完整事件流
|
|
20
|
-
app.get('/api/admin/users/:id/timeline', (req, res) => {
|
|
22
|
+
app.get('/api/admin/users/:id/timeline', async (req, res) => {
|
|
21
23
|
const admin = requireUsersAdmin(req, res);
|
|
22
24
|
if (!admin)
|
|
23
25
|
return;
|
|
@@ -29,52 +31,52 @@ export function registerAdminUsersQueryRoutes(app, deps) {
|
|
|
29
31
|
return;
|
|
30
32
|
events.push({ ts, type, icon, summary, ref_id: refId || null, ref_type: refType || null, amount: amount ?? null });
|
|
31
33
|
};
|
|
32
|
-
|
|
34
|
+
(await dbAll(`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`, [id, id, id])).forEach(o => {
|
|
33
35
|
const role = o.buyer_id === id ? '买家' : o.seller_id === id ? '卖家' : '物流';
|
|
34
36
|
push(o.created_at, 'order', '📦', `订单 (${role}) ${o.id} · ${o.total_amount} WAZ · ${o.status}`, o.id, 'order', o.total_amount);
|
|
35
37
|
});
|
|
36
|
-
|
|
38
|
+
(await dbAll(`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`, [id, id])).forEach(r => {
|
|
37
39
|
const role = r.buyer_id === id ? '给出' : '收到';
|
|
38
40
|
push(r.created_at, 'rating', '⭐', `${role} ${r.stars} 星评价 (订单 ${r.order_id})`, r.order_id, 'order');
|
|
39
41
|
});
|
|
40
|
-
|
|
42
|
+
(await dbAll(`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`, [id, id])).forEach(r => {
|
|
41
43
|
const role = r.buyer_id === id ? '发起' : '收到';
|
|
42
44
|
push(r.created_at, 'return', '↩', `${role} 退货 (${r.status}, ${r.refund_amount} WAZ, ${r.reason})`, r.order_id, 'order', r.refund_amount);
|
|
43
45
|
});
|
|
44
|
-
|
|
46
|
+
(await dbAll(`SELECT id, category, subject, status, created_at FROM feedback_tickets WHERE user_id=? ORDER BY created_at DESC LIMIT 30`, [id])).forEach(f => {
|
|
45
47
|
push(f.created_at, 'feedback', '💬', `反馈 (${f.category}/${f.status}): ${f.subject}`, f.id, 'feedback');
|
|
46
48
|
});
|
|
47
|
-
|
|
49
|
+
(await dbAll(`SELECT checkin_date, reward, streak, created_at FROM daily_checkins WHERE user_id=? ORDER BY created_at DESC LIMIT 30`, [id])).forEach(c => {
|
|
48
50
|
push(c.created_at, 'checkin', '📅', `签到 ${c.checkin_date} · streak ${c.streak} · +${c.reward} WAZ`, null, null, c.reward);
|
|
49
51
|
});
|
|
50
|
-
|
|
52
|
+
(await dbAll(`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`, [id])).forEach(tc => {
|
|
51
53
|
push(tc.claimed_at, 'task', '🎁', `任务 ${tc.task_key} 领取 +${tc.reward} WAZ`, null, null, tc.reward);
|
|
52
54
|
});
|
|
53
|
-
|
|
55
|
+
(await dbAll(`SELECT amount, source, ref, created_at FROM platform_reward_log WHERE user_id=? ORDER BY created_at DESC LIMIT 50`, [id])).forEach(p => {
|
|
54
56
|
push(p.created_at, 'reward', '💰', `平台拨付 (${p.source}${p.ref ? '/' + p.ref : ''}) +${p.amount} WAZ`, null, null, p.amount);
|
|
55
57
|
});
|
|
56
|
-
|
|
58
|
+
(await dbAll(`SELECT followee_id, created_at FROM follows WHERE follower_id=? ORDER BY created_at DESC LIMIT 20`, [id])).forEach(f => {
|
|
57
59
|
push(f.created_at, 'follow', '🤝', `关注 ${f.followee_id}`, f.followee_id, 'user');
|
|
58
60
|
});
|
|
59
|
-
|
|
61
|
+
(await dbAll(`SELECT product_id, created_at FROM user_wishlist WHERE user_id=? ORDER BY created_at DESC LIMIT 20`, [id])).forEach(w => {
|
|
60
62
|
push(w.created_at, 'wishlist', '❤', `加入心愿单 ${w.product_id}`, w.product_id, 'product');
|
|
61
63
|
});
|
|
62
|
-
|
|
64
|
+
(await dbAll(`SELECT product_id, created_at FROM product_waitlist WHERE user_id=? ORDER BY created_at DESC LIMIT 20`, [id])).forEach(w => {
|
|
63
65
|
push(w.created_at, 'waitlist', '⏰', `加入补货提醒 ${w.product_id}`, w.product_id, 'product');
|
|
64
66
|
});
|
|
65
|
-
|
|
67
|
+
(await dbAll(`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`, [id, id])).forEach(d => {
|
|
66
68
|
const role = d.initiator_id === id ? '发起' : '被诉';
|
|
67
69
|
push(d.created_at, 'dispute_open', '⚖', `${role} 争议 ${d.id} (${d.status})`, d.id, 'dispute');
|
|
68
70
|
if (d.resolved_at)
|
|
69
71
|
push(d.resolved_at, 'dispute_resolved', '⚖', `争议 ${d.id} 结案 (${d.ruling_type || '—'})`, d.id, 'dispute');
|
|
70
72
|
});
|
|
71
|
-
const userRow =
|
|
73
|
+
const userRow = await dbOne(`SELECT created_at, name, role FROM users WHERE id=?`, [id]);
|
|
72
74
|
if (userRow)
|
|
73
75
|
push(userRow.created_at, 'register', '🎉', `注册账号 ${userRow.name} (${userRow.role})`, null, null);
|
|
74
76
|
events.sort((a, b) => String(b.ts).localeCompare(String(a.ts)));
|
|
75
77
|
res.json({ items: events.slice(0, limit), total: events.length });
|
|
76
78
|
});
|
|
77
|
-
app.post('/api/admin/users/batch-action', (req, res) => {
|
|
79
|
+
app.post('/api/admin/users/batch-action', async (req, res) => {
|
|
78
80
|
const admin = requireUsersAdmin(req, res);
|
|
79
81
|
if (!admin)
|
|
80
82
|
return;
|
|
@@ -94,13 +96,12 @@ export function registerAdminUsersQueryRoutes(app, deps) {
|
|
|
94
96
|
continue;
|
|
95
97
|
}
|
|
96
98
|
if (action === 'suspend') {
|
|
97
|
-
|
|
99
|
+
await dbRun(`INSERT INTO user_moderation (user_id, suspended, reason, suspended_by, suspended_at)
|
|
98
100
|
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
|
+
ON CONFLICT(user_id) DO UPDATE SET suspended = 1, reason = excluded.reason, suspended_by = excluded.suspended_by, suspended_at = excluded.suspended_at`, [uid, reasonStr, admin.id]);
|
|
101
102
|
}
|
|
102
103
|
else {
|
|
103
|
-
|
|
104
|
+
await dbRun(`UPDATE user_moderation SET suspended = 0 WHERE user_id = ?`, [uid]);
|
|
104
105
|
}
|
|
105
106
|
results.push({ user_id: uid, status: 'ok' });
|
|
106
107
|
}
|
|
@@ -109,13 +110,24 @@ export function registerAdminUsersQueryRoutes(app, deps) {
|
|
|
109
110
|
}
|
|
110
111
|
}
|
|
111
112
|
const ok = results.filter(r => r.status === 'ok').length;
|
|
113
|
+
try {
|
|
114
|
+
// 防审计行膨胀:批量上限本就 ≤200,但仍只记前 50 个 id + 计数(truncated 标记),保证单行有界。
|
|
115
|
+
const okIds = results.filter(r => r.status === 'ok').map(r => r.user_id);
|
|
116
|
+
logAdminAction(admin.id, 'users_batch_' + String(action), 'user', null, {
|
|
117
|
+
action, reason: reasonStr, applied: ok, requested: user_ids.length,
|
|
118
|
+
user_ids: okIds.slice(0, 50), user_ids_truncated: okIds.length > 50,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
catch (e) {
|
|
122
|
+
console.error('[users_batch audit]', e);
|
|
123
|
+
}
|
|
112
124
|
try {
|
|
113
125
|
broadcastSystemEvent('admin_bulk_' + action, '🛡', `${admin.id} 批量${action === 'suspend' ? '暂停' : '解封'} ${ok} 用户`, null);
|
|
114
126
|
}
|
|
115
127
|
catch { }
|
|
116
128
|
res.json({ success: true, applied: ok, results });
|
|
117
129
|
});
|
|
118
|
-
app.get('/api/admin/users', (req, res) => {
|
|
130
|
+
app.get('/api/admin/users', async (req, res) => {
|
|
119
131
|
const admin = requireUsersAdmin(req, res);
|
|
120
132
|
if (!admin)
|
|
121
133
|
return;
|
|
@@ -175,7 +187,7 @@ export function registerAdminUsersQueryRoutes(app, deps) {
|
|
|
175
187
|
}
|
|
176
188
|
}
|
|
177
189
|
sql += ` ORDER BY u.created_at DESC LIMIT 100`;
|
|
178
|
-
const rows =
|
|
190
|
+
const rows = await dbAll(sql, params);
|
|
179
191
|
res.json({
|
|
180
192
|
match_mode,
|
|
181
193
|
my_admin_type: admin.admin_type || 'root',
|
|
@@ -208,21 +220,21 @@ export function registerAdminUsersQueryRoutes(app, deps) {
|
|
|
208
220
|
});
|
|
209
221
|
});
|
|
210
222
|
// 完整档案聚合
|
|
211
|
-
app.get('/api/admin/users/:id/profile', (req, res) => {
|
|
223
|
+
app.get('/api/admin/users/:id/profile', async (req, res) => {
|
|
212
224
|
const admin = requireUsersAdmin(req, res);
|
|
213
225
|
if (!admin)
|
|
214
226
|
return;
|
|
215
227
|
if (!adminCanOperateOn(admin, req.params.id, res))
|
|
216
228
|
return;
|
|
217
229
|
const id = req.params.id;
|
|
218
|
-
const user =
|
|
230
|
+
const user = await dbOne("SELECT * FROM users WHERE id = ?", [id]);
|
|
219
231
|
if (!user)
|
|
220
232
|
return void res.json({ error: '用户不存在' });
|
|
221
|
-
const wallet =
|
|
222
|
-
const mod =
|
|
223
|
-
const vw =
|
|
224
|
-
const vs =
|
|
225
|
-
const vAppPending = !!
|
|
233
|
+
const wallet = await dbOne("SELECT balance, staked, escrowed, earned, deposit_address FROM wallets WHERE user_id = ?", [id]);
|
|
234
|
+
const mod = await dbOne("SELECT suspended, reason, suspended_by, suspended_at FROM user_moderation WHERE user_id = ?", [id]);
|
|
235
|
+
const vw = await dbOne("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 = ?", [id]);
|
|
236
|
+
const vs = await dbOne("SELECT verify_rights, tasks_done, tasks_correct, tasks_wrong, suspended_until FROM verifier_stats WHERE user_id = ?", [id]);
|
|
237
|
+
const vAppPending = !!(await dbOne("SELECT 1 FROM verifier_applications WHERE user_id = ? AND status='pending' LIMIT 1", [id]));
|
|
226
238
|
const roleSet = new Set((() => { try {
|
|
227
239
|
return JSON.parse(user.roles || '[]');
|
|
228
240
|
}
|
|
@@ -231,20 +243,20 @@ export function registerAdminUsersQueryRoutes(app, deps) {
|
|
|
231
243
|
} })());
|
|
232
244
|
const kpis = {};
|
|
233
245
|
if (roleSet.has('seller')) {
|
|
234
|
-
const p =
|
|
246
|
+
const p = (await dbOne(`SELECT COUNT(*) as total,
|
|
235
247
|
SUM(CASE WHEN status='active' THEN 1 ELSE 0 END) as active,
|
|
236
248
|
SUM(CASE WHEN status='paused' THEN 1 ELSE 0 END) as paused,
|
|
237
249
|
SUM(CASE WHEN status='deleted'THEN 1 ELSE 0 END) as deleted
|
|
238
|
-
FROM products WHERE seller_id =
|
|
239
|
-
const o =
|
|
250
|
+
FROM products WHERE seller_id = ?`, [id]));
|
|
251
|
+
const o = (await dbOne(`SELECT COUNT(*) as total,
|
|
240
252
|
SUM(CASE WHEN status='completed' THEN 1 ELSE 0 END) as completed,
|
|
241
253
|
COALESCE(SUM(CASE WHEN status='completed' THEN total_amount ELSE 0 END),0) as total_sales
|
|
242
|
-
FROM orders WHERE seller_id =
|
|
243
|
-
const d =
|
|
254
|
+
FROM orders WHERE seller_id = ?`, [id]));
|
|
255
|
+
const d = (await dbOne(`SELECT COUNT(*) as defendant_count,
|
|
244
256
|
SUM(CASE WHEN ruling_type IN ('refund_buyer','partial_refund') THEN 1 ELSE 0 END) as lost
|
|
245
|
-
FROM disputes WHERE defendant_id =
|
|
257
|
+
FROM disputes WHERE defendant_id = ?`, [id]));
|
|
246
258
|
const today = todayStartISO();
|
|
247
|
-
const todayCount =
|
|
259
|
+
const todayCount = (await dbOne("SELECT COUNT(*) as n FROM products WHERE seller_id = ? AND created_at >= ?", [id, today])).n;
|
|
248
260
|
const dailyLimit = getSellerDailyLimit({ id, created_at: user.created_at });
|
|
249
261
|
kpis.seller = {
|
|
250
262
|
products_total: p.total, products_active: p.active, products_paused: p.paused, products_deleted: p.deleted,
|
|
@@ -257,20 +269,20 @@ export function registerAdminUsersQueryRoutes(app, deps) {
|
|
|
257
269
|
};
|
|
258
270
|
}
|
|
259
271
|
if (roleSet.has('buyer')) {
|
|
260
|
-
const o =
|
|
272
|
+
const o = (await dbOne(`SELECT COUNT(*) as total,
|
|
261
273
|
SUM(CASE WHEN status='completed' THEN 1 ELSE 0 END) as completed,
|
|
262
274
|
COALESCE(SUM(CASE WHEN status='completed' THEN total_amount ELSE 0 END),0) as total_spent,
|
|
263
275
|
MAX(created_at) as last_order_at
|
|
264
|
-
FROM orders WHERE buyer_id =
|
|
276
|
+
FROM orders WHERE buyer_id = ?`, [id]));
|
|
265
277
|
kpis.buyer = {
|
|
266
278
|
orders_total: o.total, orders_completed: o.completed,
|
|
267
279
|
total_spent: o.total_spent, last_order_at: o.last_order_at,
|
|
268
280
|
};
|
|
269
281
|
}
|
|
270
282
|
if (roleSet.has('logistics')) {
|
|
271
|
-
const o =
|
|
283
|
+
const o = (await dbOne(`SELECT COUNT(*) as total,
|
|
272
284
|
SUM(CASE WHEN status='completed' THEN 1 ELSE 0 END) as completed
|
|
273
|
-
FROM orders WHERE logistics_id =
|
|
285
|
+
FROM orders WHERE logistics_id = ?`, [id]));
|
|
274
286
|
kpis.logistics = { deliveries_total: o.total, deliveries_completed: o.completed };
|
|
275
287
|
}
|
|
276
288
|
if (roleSet.has('verifier') && vw) {
|
|
@@ -296,28 +308,28 @@ export function registerAdminUsersQueryRoutes(app, deps) {
|
|
|
296
308
|
return;
|
|
297
309
|
events.push({ ts, type, icon, summary, ref_id: refId, ref_type: refType });
|
|
298
310
|
};
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
311
|
+
(await dbAll(`SELECT id, total_amount, product_id, status, created_at FROM orders WHERE buyer_id = ? ORDER BY created_at DESC LIMIT 10`, [id])).forEach(o => pushEvt(o.created_at, 'order_buy', '🛒', `下单 ${o.total_amount} WAZ (${o.status})`, o.id, 'order'));
|
|
312
|
+
(await dbAll(`SELECT id, total_amount, status, created_at FROM orders WHERE seller_id = ? ORDER BY created_at DESC LIMIT 10`, [id])).forEach(o => pushEvt(o.created_at, 'order_sell', '💰', `售出 ${o.total_amount} WAZ (${o.status})`, o.id, 'order'));
|
|
313
|
+
(await dbAll(`SELECT id, title, status, created_at FROM products WHERE seller_id = ? ORDER BY created_at DESC LIMIT 10`, [id])).forEach(p => pushEvt(p.created_at, 'product_listed', '🏪', `上架商品 ${(p.title || '').slice(0, 30)}`, p.id, 'product'));
|
|
314
|
+
(await dbAll(`SELECT id, task_id, verdict, claimed_at, submitted_at FROM verify_submissions WHERE verifier_id = ? ORDER BY claimed_at DESC LIMIT 10`, [id])).forEach(s => {
|
|
303
315
|
pushEvt(s.claimed_at, 'verify_claimed', '🔍', `认领验证任务`, s.task_id, 'task');
|
|
304
316
|
if (s.submitted_at) {
|
|
305
317
|
const icon = s.verdict === 'correct' ? '✓' : s.verdict === 'wrong' ? '✗' : '⏳';
|
|
306
318
|
pushEvt(s.submitted_at, 'verify_submitted', icon, `提交验证 (${s.verdict || 'pending'})`, s.task_id, 'task');
|
|
307
319
|
}
|
|
308
320
|
});
|
|
309
|
-
|
|
321
|
+
(await dbAll(`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`, [id, id])).forEach(d => {
|
|
310
322
|
const role = d.initiator_id === id ? '发起' : '被告';
|
|
311
323
|
pushEvt(d.created_at, 'dispute_init', '⚖', `争议 ${role} (${d.status})`, d.id, 'dispute');
|
|
312
324
|
if (d.resolved_at)
|
|
313
325
|
pushEvt(d.resolved_at, 'dispute_resolved', '⚖', `争议结案 (${d.ruling_type || '—'})`, d.id, 'dispute');
|
|
314
326
|
});
|
|
315
|
-
|
|
327
|
+
(await dbAll(`SELECT id, status, decision_note, applied_at, reviewed_at FROM verifier_applications WHERE user_id = ? ORDER BY applied_at DESC LIMIT 5`, [id])).forEach(a => {
|
|
316
328
|
pushEvt(a.applied_at, 'verifier_apply', '📥', `提交审核员申请`, a.id, 'verifier_app');
|
|
317
329
|
if (a.reviewed_at)
|
|
318
330
|
pushEvt(a.reviewed_at, 'verifier_review', a.status === 'approved' ? '✅' : '❌', `申请${a.status === 'approved' ? '获批' : a.status === 'rejected' ? '被拒' : a.status}`, a.id, 'verifier_app');
|
|
319
331
|
});
|
|
320
|
-
|
|
332
|
+
(await dbAll(`SELECT id, status, created_at, reviewed_at FROM verifier_appeals WHERE user_id = ? ORDER BY created_at DESC LIMIT 5`, [id])).forEach(a => {
|
|
321
333
|
pushEvt(a.created_at, 'appeal_submitted', '📩', `提交申诉`, a.id, 'appeal');
|
|
322
334
|
if (a.reviewed_at)
|
|
323
335
|
pushEvt(a.reviewed_at, 'appeal_decided', a.status === 'accepted' ? '✅' : '❌', `申诉${a.status === 'accepted' ? '成立' : '驳回'}`, a.id, 'appeal');
|
|
@@ -332,10 +344,10 @@ export function registerAdminUsersQueryRoutes(app, deps) {
|
|
|
332
344
|
if (user.locked_until && new Date(user.locked_until).getTime() > Date.now()) {
|
|
333
345
|
risks.push({ severity: 'high', label: '账户已锁定', detail: `解锁: ${user.locked_until}` });
|
|
334
346
|
}
|
|
335
|
-
const openDisputes =
|
|
347
|
+
const openDisputes = (await dbOne("SELECT COUNT(*) as n FROM disputes WHERE defendant_id = ? AND status IN ('open','in_review')", [id])).n;
|
|
336
348
|
if (openDisputes > 0)
|
|
337
349
|
risks.push({ severity: 'medium', label: '未结争议作为被告', detail: `${openDisputes} 起` });
|
|
338
|
-
const lostCount =
|
|
350
|
+
const lostCount = (await dbOne("SELECT COUNT(*) as n FROM disputes WHERE defendant_id = ? AND ruling_type IN ('refund_buyer','partial_refund')", [id])).n;
|
|
339
351
|
if (lostCount > 0)
|
|
340
352
|
risks.push({ severity: 'low', label: '历史仲裁判输', detail: `${lostCount} 次` });
|
|
341
353
|
if (wallet && Number(wallet.staked) > Number(wallet.balance) * 2) {
|
|
@@ -345,14 +357,14 @@ export function registerAdminUsersQueryRoutes(app, deps) {
|
|
|
345
357
|
const ec = Number(vw.error_count_180d);
|
|
346
358
|
risks.push({ severity: ec >= 2 ? 'high' : 'medium', label: '审核员近期错误', detail: `180 天 ${ec} 次` });
|
|
347
359
|
}
|
|
348
|
-
const audit =
|
|
360
|
+
const audit = (await dbAll(`
|
|
349
361
|
SELECT al.id, al.admin_id, al.action, al.detail, al.created_at,
|
|
350
362
|
u.name as admin_name
|
|
351
363
|
FROM admin_audit_log al
|
|
352
364
|
LEFT JOIN users u ON u.id = al.admin_id
|
|
353
365
|
WHERE al.target_type = 'user' AND al.target_id = ?
|
|
354
366
|
ORDER BY al.created_at DESC LIMIT 20
|
|
355
|
-
|
|
367
|
+
`, [id])).map(r => ({
|
|
356
368
|
...r,
|
|
357
369
|
detail: r.detail ? (() => { try {
|
|
358
370
|
return JSON.parse(r.detail);
|
|
@@ -1,18 +1,21 @@
|
|
|
1
|
+
import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
export function registerAdminVerifierFlowRoutes(app, deps) {
|
|
3
|
+
// 只读站点走 RFC-016 异步 seam;db 保留:approve/reject/decide 含状态翻转 + 退质押 + 补发奖励,
|
|
4
|
+
// 必须原子(db.transaction + CAS 翻转,退款/奖励仅在本请求真翻转时落),Phase 3 迁 pg 行锁。
|
|
2
5
|
const { db, requireVerifierMgmtAdmin, TIER_QUOTAS, VERIFIER_STAKE_REQUIRED, todayStartISO, logAdminAction } = deps;
|
|
3
|
-
app.get('/api/admin/verifier-applications', (req, res) => {
|
|
6
|
+
app.get('/api/admin/verifier-applications', async (req, res) => {
|
|
4
7
|
const admin = requireVerifierMgmtAdmin(req, res);
|
|
5
8
|
if (!admin)
|
|
6
9
|
return;
|
|
7
10
|
const status = req.query.status || 'pending';
|
|
8
|
-
const rows =
|
|
11
|
+
const rows = await dbAll(`
|
|
9
12
|
SELECT va.id, va.user_id, va.status, va.applied_at, va.reviewed_at, va.reviewed_by, va.decision_note, va.snapshot,
|
|
10
13
|
u.name as user_name, u.email
|
|
11
14
|
FROM verifier_applications va
|
|
12
15
|
LEFT JOIN users u ON u.id = va.user_id
|
|
13
16
|
WHERE va.status = ?
|
|
14
17
|
ORDER BY va.applied_at DESC LIMIT 100
|
|
15
|
-
|
|
18
|
+
`, [status]);
|
|
16
19
|
res.json({
|
|
17
20
|
applications: rows.map(r => ({
|
|
18
21
|
...r,
|
|
@@ -25,59 +28,83 @@ export function registerAdminVerifierFlowRoutes(app, deps) {
|
|
|
25
28
|
})),
|
|
26
29
|
});
|
|
27
30
|
});
|
|
28
|
-
app.post('/api/admin/verifier-applications/:id/approve', (req, res) => {
|
|
31
|
+
app.post('/api/admin/verifier-applications/:id/approve', async (req, res) => {
|
|
29
32
|
const admin = requireVerifierMgmtAdmin(req, res);
|
|
30
33
|
if (!admin)
|
|
31
34
|
return;
|
|
32
35
|
const { tier, note } = req.body;
|
|
33
36
|
const validTier = ['trial-1', 'trial-2', 'trial-3', 'active-1', 'active-2'].includes(tier) ? tier : 'trial-1';
|
|
34
|
-
const apl =
|
|
37
|
+
const apl = await dbOne("SELECT id, user_id, status FROM verifier_applications WHERE id = ?", [req.params.id]);
|
|
35
38
|
if (!apl)
|
|
36
39
|
return void res.json({ error: '申请不存在' });
|
|
37
40
|
if (apl.status !== 'pending')
|
|
38
41
|
return void res.json({ error: '该申请不在待审状态' });
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
// 原子段:CAS 翻转 pending→approved + 入白名单 + 建 stats(防并发双批准)
|
|
43
|
+
try {
|
|
44
|
+
db.transaction(() => {
|
|
45
|
+
const cas = db.prepare("UPDATE verifier_applications SET status='approved', reviewed_at=datetime('now'), reviewed_by=?, decision_note=? WHERE id=? AND status='pending'")
|
|
46
|
+
.run(admin.id, note || null, apl.id);
|
|
47
|
+
if (cas.changes === 0)
|
|
48
|
+
throw new Error('APP_RACE');
|
|
49
|
+
db.prepare(`INSERT OR REPLACE INTO verifier_whitelist
|
|
50
|
+
(user_id, note, tier, daily_quota, tasks_today, quota_reset_at, granted_by, stake_amount, is_system)
|
|
51
|
+
VALUES (?,?,?,?,0,?,?,?,0)`)
|
|
52
|
+
.run(apl.user_id, note || `批准为 ${validTier}`, validTier, TIER_QUOTAS[validTier], todayStartISO(), admin.id, VERIFIER_STAKE_REQUIRED);
|
|
53
|
+
db.prepare("INSERT OR IGNORE INTO verifier_stats (user_id) VALUES (?)").run(apl.user_id);
|
|
54
|
+
})();
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
if (e.message === 'APP_RACE')
|
|
58
|
+
return void res.json({ error: '该申请不在待审状态' });
|
|
59
|
+
console.error('[verifier approve tx]', e.message);
|
|
60
|
+
return void res.status(500).json({ error: '批准失败,请重试' });
|
|
61
|
+
}
|
|
46
62
|
logAdminAction(admin.id, 'approve_verifier', 'user', apl.user_id, { tier: validTier, note });
|
|
47
63
|
res.json({ success: true });
|
|
48
64
|
});
|
|
49
|
-
app.post('/api/admin/verifier-applications/:id/reject', (req, res) => {
|
|
65
|
+
app.post('/api/admin/verifier-applications/:id/reject', async (req, res) => {
|
|
50
66
|
const admin = requireVerifierMgmtAdmin(req, res);
|
|
51
67
|
if (!admin)
|
|
52
68
|
return;
|
|
53
69
|
const { note } = req.body;
|
|
54
|
-
const apl =
|
|
70
|
+
const apl = await dbOne("SELECT id, user_id, status FROM verifier_applications WHERE id = ?", [req.params.id]);
|
|
55
71
|
if (!apl)
|
|
56
72
|
return void res.json({ error: '申请不存在' });
|
|
57
73
|
if (apl.status !== 'pending')
|
|
58
74
|
return void res.json({ error: '该申请不在待审状态' });
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
.
|
|
75
|
+
// 原子段:CAS 翻转 pending→rejected + 退质押仅在本请求真翻转时(防并发双拒双退)
|
|
76
|
+
try {
|
|
77
|
+
db.transaction(() => {
|
|
78
|
+
const cas = db.prepare("UPDATE verifier_applications SET status='rejected', reviewed_at=datetime('now'), reviewed_by=?, decision_note=? WHERE id=? AND status='pending'")
|
|
79
|
+
.run(admin.id, note || null, apl.id);
|
|
80
|
+
if (cas.changes === 0)
|
|
81
|
+
throw new Error('APP_RACE');
|
|
82
|
+
if (VERIFIER_STAKE_REQUIRED > 0) {
|
|
83
|
+
db.prepare("UPDATE wallets SET balance = balance + ?, staked = staked - ? WHERE user_id = ?").run(VERIFIER_STAKE_REQUIRED, VERIFIER_STAKE_REQUIRED, apl.user_id);
|
|
84
|
+
}
|
|
85
|
+
})();
|
|
86
|
+
}
|
|
87
|
+
catch (e) {
|
|
88
|
+
if (e.message === 'APP_RACE')
|
|
89
|
+
return void res.json({ error: '该申请不在待审状态' });
|
|
90
|
+
console.error('[verifier reject tx]', e.message);
|
|
91
|
+
return void res.status(500).json({ error: '拒绝失败,请重试' });
|
|
65
92
|
}
|
|
66
93
|
logAdminAction(admin.id, 'reject_verifier', 'user', apl.user_id, { note });
|
|
67
94
|
res.json({ success: true });
|
|
68
95
|
});
|
|
69
|
-
app.get('/api/admin/verifier-appeals', (req, res) => {
|
|
96
|
+
app.get('/api/admin/verifier-appeals', async (req, res) => {
|
|
70
97
|
const admin = requireVerifierMgmtAdmin(req, res);
|
|
71
98
|
if (!admin)
|
|
72
99
|
return;
|
|
73
100
|
const status = req.query.status || 'pending';
|
|
74
|
-
const rows =
|
|
101
|
+
const rows = await dbAll(`
|
|
75
102
|
SELECT va.id, va.user_id, va.task_id, va.submission_id, va.reason, va.evidence_urls, va.status,
|
|
76
103
|
va.admin_note, va.reviewed_by, va.reviewed_at, va.created_at, u.name as user_name
|
|
77
104
|
FROM verifier_appeals va LEFT JOIN users u ON u.id = va.user_id
|
|
78
105
|
WHERE va.status = ?
|
|
79
106
|
ORDER BY va.created_at DESC LIMIT 100
|
|
80
|
-
|
|
107
|
+
`, [status]);
|
|
81
108
|
res.json({
|
|
82
109
|
appeals: rows.map(r => ({
|
|
83
110
|
...r,
|
|
@@ -90,35 +117,49 @@ export function registerAdminVerifierFlowRoutes(app, deps) {
|
|
|
90
117
|
})),
|
|
91
118
|
});
|
|
92
119
|
});
|
|
93
|
-
app.post('/api/admin/verifier-appeals/:id/decide', (req, res) => {
|
|
120
|
+
app.post('/api/admin/verifier-appeals/:id/decide', async (req, res) => {
|
|
94
121
|
const admin = requireVerifierMgmtAdmin(req, res);
|
|
95
122
|
if (!admin)
|
|
96
123
|
return;
|
|
97
124
|
const { decision, note } = req.body; // 'accepted' | 'rejected'
|
|
98
125
|
if (!['accepted', 'rejected'].includes(decision))
|
|
99
126
|
return void res.json({ error: 'decision 无效' });
|
|
100
|
-
const appeal =
|
|
127
|
+
const appeal = await dbOne("SELECT id, user_id, status FROM verifier_appeals WHERE id = ?", [req.params.id]);
|
|
101
128
|
if (!appeal)
|
|
102
129
|
return void res.json({ error: '申诉不存在' });
|
|
103
130
|
if (appeal.status !== 'pending')
|
|
104
131
|
return void res.json({ error: '该申诉已处理' });
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
.
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
db.prepare("
|
|
119
|
-
|
|
132
|
+
// 原子段:CAS 翻转 appeal pending→decision + (accepted 时)解封/重审/补发奖励 一起落,
|
|
133
|
+
// 防并发双裁决导致 verify_rights 多加、verdict 多翻、奖励多发。
|
|
134
|
+
try {
|
|
135
|
+
db.transaction(() => {
|
|
136
|
+
const cas = db.prepare("UPDATE verifier_appeals SET status = ?, admin_note = ?, reviewed_by = ?, reviewed_at = datetime('now') WHERE id = ? AND status = 'pending'")
|
|
137
|
+
.run(decision, note || null, admin.id, appeal.id);
|
|
138
|
+
if (cas.changes === 0)
|
|
139
|
+
throw new Error('APPEAL_RACE');
|
|
140
|
+
if (decision === 'accepted') {
|
|
141
|
+
// 解封 + 验证权 +2 + 错误次数 -1
|
|
142
|
+
db.prepare("UPDATE verifier_stats SET suspended_until = NULL, verify_rights = verify_rights + 2 WHERE user_id = ?").run(appeal.user_id);
|
|
143
|
+
db.prepare("UPDATE verifier_whitelist SET error_count_180d = MAX(0, error_count_180d - 1) WHERE user_id = ?").run(appeal.user_id);
|
|
144
|
+
// 完整重审:翻转该 verifier 在该 task 的 verdict + 补发奖励 + 翻回 stats
|
|
145
|
+
const fullAppeal = db.prepare("SELECT task_id FROM verifier_appeals WHERE id = ?").get(appeal.id);
|
|
146
|
+
if (fullAppeal?.task_id) {
|
|
147
|
+
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 = ?")
|
|
148
|
+
.get(fullAppeal.task_id, appeal.user_id);
|
|
149
|
+
if (sub && sub.verdict === 'wrong') {
|
|
150
|
+
db.prepare("UPDATE verify_submissions SET verdict = 'correct' WHERE id = ?").run(sub.id);
|
|
151
|
+
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);
|
|
152
|
+
db.prepare("UPDATE wallets SET balance = balance + ? WHERE user_id = ?").run(sub.reward_per_verifier, appeal.user_id);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
120
155
|
}
|
|
121
|
-
}
|
|
156
|
+
})();
|
|
157
|
+
}
|
|
158
|
+
catch (e) {
|
|
159
|
+
if (e.message === 'APPEAL_RACE')
|
|
160
|
+
return void res.json({ error: '该申诉已处理' });
|
|
161
|
+
console.error('[verifier appeal decide tx]', e.message);
|
|
162
|
+
return void res.status(500).json({ error: '裁决失败,请重试' });
|
|
122
163
|
}
|
|
123
164
|
logAdminAction(admin.id, 'decide_appeal', 'user', appeal.user_id, { decision, note: note || null });
|
|
124
165
|
res.json({ success: true });
|