@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,21 +1,23 @@
|
|
|
1
|
+
import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
export function registerUsersPublicRoutes(app, deps) {
|
|
2
|
-
|
|
3
|
+
// db 已全量走 RFC-016 异步 seam(dbOne/dbAll),不再直接用 deps.db
|
|
4
|
+
const { auth, noteAuthenticityBadges } = deps;
|
|
3
5
|
// ref → user id(usr_xxx / permanent_code / @handle 三态)
|
|
4
|
-
const resolveUserId = (ref0) => {
|
|
6
|
+
const resolveUserId = async (ref0) => {
|
|
5
7
|
const ref = String(ref0 || '').trim();
|
|
6
8
|
if (/^usr_[A-Za-z0-9_]+$/.test(ref))
|
|
7
9
|
return ref;
|
|
8
10
|
if (/^[A-Z0-9]{6,7}$/i.test(ref) && !ref.startsWith('@')) {
|
|
9
|
-
const r =
|
|
11
|
+
const r = await dbOne("SELECT id FROM users WHERE permanent_code = ?", [ref.toUpperCase()]);
|
|
10
12
|
if (r)
|
|
11
13
|
return r.id;
|
|
12
14
|
}
|
|
13
15
|
const h = ref.replace(/^@/, '').toLowerCase();
|
|
14
|
-
const r =
|
|
16
|
+
const r = await dbOne("SELECT id FROM users WHERE handle = ?", [h]);
|
|
15
17
|
return r ? r.id : null;
|
|
16
18
|
};
|
|
17
19
|
// 公开 reputation — 仅 level
|
|
18
|
-
app.get('/api/users/:id/reputation', (req, res) => {
|
|
20
|
+
app.get('/api/users/:id/reputation', async (req, res) => {
|
|
19
21
|
let userId = req.params.id;
|
|
20
22
|
if (userId === 'me') {
|
|
21
23
|
const user = auth(req, res);
|
|
@@ -23,17 +25,17 @@ export function registerUsersPublicRoutes(app, deps) {
|
|
|
23
25
|
return;
|
|
24
26
|
userId = user.id;
|
|
25
27
|
}
|
|
26
|
-
const row =
|
|
28
|
+
const row = await dbOne(`
|
|
27
29
|
SELECT level, MAX(trust_score) as max_score
|
|
28
30
|
FROM agent_reputation WHERE user_id = ?
|
|
29
31
|
GROUP BY user_id
|
|
30
|
-
|
|
32
|
+
`, [userId]);
|
|
31
33
|
if (!row)
|
|
32
34
|
return void res.json({ user_id: userId, level: 'new' });
|
|
33
35
|
res.json({ user_id: userId, level: row.level });
|
|
34
36
|
});
|
|
35
37
|
// PV 简报:组织图点击节点用
|
|
36
|
-
app.get('/api/users/:id/pv-summary', (req, res) => {
|
|
38
|
+
app.get('/api/users/:id/pv-summary', async (req, res) => {
|
|
37
39
|
const me = auth(req, res);
|
|
38
40
|
if (!me)
|
|
39
41
|
return;
|
|
@@ -42,32 +44,26 @@ export function registerUsersPublicRoutes(app, deps) {
|
|
|
42
44
|
if (/^usr_[A-Za-z0-9_]+$/.test(ref))
|
|
43
45
|
targetId = ref;
|
|
44
46
|
else if (/^[A-Z0-9]{6,7}$/i.test(ref) && !ref.startsWith('@')) {
|
|
45
|
-
const r =
|
|
47
|
+
const r = await dbOne("SELECT id FROM users WHERE permanent_code = ?", [ref.toUpperCase()]);
|
|
46
48
|
if (r)
|
|
47
49
|
targetId = r.id;
|
|
48
50
|
}
|
|
49
51
|
if (!targetId)
|
|
50
52
|
return void res.json({ error: 'user not found' });
|
|
51
|
-
const u =
|
|
53
|
+
const u = await dbOne(`
|
|
52
54
|
SELECT id, name, permanent_code, handle, total_left_pv, total_right_pv,
|
|
53
55
|
placement_id, placement_side, placement_depth, left_child_id, right_child_id, created_at
|
|
54
56
|
FROM users WHERE id = ?
|
|
55
|
-
|
|
57
|
+
`, [targetId]);
|
|
56
58
|
if (!u)
|
|
57
59
|
return void res.json({ error: 'user not found' });
|
|
58
|
-
const placementName = u.placement_id ?
|
|
59
|
-
const leftChildName = u.left_child_id ?
|
|
60
|
-
const rightChildName = u.right_child_id ?
|
|
61
|
-
const score = db.prepare(`
|
|
62
|
-
SELECT
|
|
63
|
-
COALESCE(SUM(CASE WHEN settled_at IS NULL THEN score ELSE 0 END),0) AS pending_score,
|
|
64
|
-
COALESCE(SUM(CASE WHEN settled_at IS NOT NULL THEN waz_amount ELSE 0 END),0) AS settled_waz,
|
|
65
|
-
COUNT(*) AS total_hits
|
|
66
|
-
FROM binary_score_records WHERE user_id = ?
|
|
67
|
-
`).get(targetId);
|
|
60
|
+
const placementName = u.placement_id ? (await dbOne("SELECT name FROM users WHERE id = ?", [u.placement_id]))?.name : null;
|
|
61
|
+
const leftChildName = u.left_child_id ? (await dbOne("SELECT name FROM users WHERE id = ?", [u.left_child_id]))?.name : null;
|
|
62
|
+
const rightChildName = u.right_child_id ? (await dbOne("SELECT name FROM users WHERE id = ?", [u.right_child_id]))?.name : null;
|
|
68
63
|
const leftPv = Number(u.total_left_pv ?? 0);
|
|
69
64
|
const rightPv = Number(u.total_right_pv ?? 0);
|
|
70
|
-
|
|
65
|
+
// pre-public de-MLM: PV 对碰为 pre-launch、未启用 —— public 端口不再暴露弱腿 / 对碰收益指标
|
|
66
|
+
// (weak_leg_pv / pending_score / settled_waz / total_hits)。位置 + 左右区 PV 仅作为参与记录保留。
|
|
71
67
|
res.json({
|
|
72
68
|
id: u.id,
|
|
73
69
|
name: u.name,
|
|
@@ -78,33 +74,29 @@ export function registerUsersPublicRoutes(app, deps) {
|
|
|
78
74
|
right_child: u.right_child_id ? { id: u.right_child_id, name: rightChildName } : null,
|
|
79
75
|
total_left_pv: leftPv,
|
|
80
76
|
total_right_pv: rightPv,
|
|
81
|
-
weak_leg_pv: weak,
|
|
82
|
-
pending_score: Number(score.pending_score),
|
|
83
|
-
settled_waz: Number(score.settled_waz),
|
|
84
|
-
total_hits: Number(score.total_hits),
|
|
85
77
|
joined_at: u.created_at,
|
|
86
78
|
});
|
|
87
79
|
});
|
|
88
80
|
// 用户公开 shareables
|
|
89
|
-
app.get('/api/users/:id/shareables', (req, res) => {
|
|
81
|
+
app.get('/api/users/:id/shareables', async (req, res) => {
|
|
90
82
|
const ref = String(req.params.id || '').trim();
|
|
91
83
|
let ownerId = null;
|
|
92
84
|
if (/^usr_[A-Za-z0-9_]+$/.test(ref))
|
|
93
85
|
ownerId = ref;
|
|
94
86
|
else if (/^[A-Z0-9]{6,7}$/i.test(ref) && !ref.startsWith('@')) {
|
|
95
|
-
const r =
|
|
87
|
+
const r = await dbOne("SELECT id FROM users WHERE permanent_code = ?", [ref.toUpperCase()]);
|
|
96
88
|
if (r)
|
|
97
89
|
ownerId = r.id;
|
|
98
90
|
}
|
|
99
91
|
if (!ownerId) {
|
|
100
92
|
const h = ref.replace(/^@/, '').toLowerCase();
|
|
101
|
-
const r =
|
|
93
|
+
const r = await dbOne("SELECT id FROM users WHERE handle = ?", [h]);
|
|
102
94
|
if (r)
|
|
103
95
|
ownerId = r.id;
|
|
104
96
|
}
|
|
105
97
|
if (!ownerId)
|
|
106
98
|
return void res.status(404).json({ error: 'user not found' });
|
|
107
|
-
const rows =
|
|
99
|
+
const rows = await dbAll(`
|
|
108
100
|
SELECT s.id, s.owner_id, s.owner_code, s.type, s.external_url, s.external_platform, s.external_video_id,
|
|
109
101
|
s.thumbnail_url, s.title, s.description, s.related_product_id, s.related_anchor,
|
|
110
102
|
s.related_order_id, s.photo_hashes,
|
|
@@ -114,68 +106,68 @@ export function registerUsersPublicRoutes(app, deps) {
|
|
|
114
106
|
LEFT JOIN products p ON p.id = s.related_product_id
|
|
115
107
|
WHERE s.owner_id = ? AND s.status = 'active'
|
|
116
108
|
ORDER BY s.created_at DESC LIMIT 100
|
|
117
|
-
|
|
109
|
+
`, [ownerId]);
|
|
118
110
|
for (const r of rows) {
|
|
119
111
|
r.badges = noteAuthenticityBadges(r);
|
|
120
112
|
}
|
|
121
113
|
res.json({ shareables: rows });
|
|
122
114
|
});
|
|
123
115
|
// 用户在售二手(公开:available + reserved)
|
|
124
|
-
app.get('/api/users/:id/secondhand', (req, res) => {
|
|
125
|
-
const ownerId = resolveUserId(req.params.id);
|
|
116
|
+
app.get('/api/users/:id/secondhand', async (req, res) => {
|
|
117
|
+
const ownerId = await resolveUserId(req.params.id);
|
|
126
118
|
if (!ownerId)
|
|
127
119
|
return void res.status(404).json({ error: 'user not found' });
|
|
128
|
-
const items =
|
|
120
|
+
const items = await dbAll(`
|
|
129
121
|
SELECT id, title, price, condition_grade, images, status, category, created_at
|
|
130
122
|
FROM secondhand_items
|
|
131
123
|
WHERE seller_id = ? AND status IN ('available', 'reserved')
|
|
132
124
|
ORDER BY created_at DESC LIMIT 50
|
|
133
|
-
|
|
125
|
+
`, [ownerId]);
|
|
134
126
|
res.json({ items });
|
|
135
127
|
});
|
|
136
128
|
// 用户进行中拍卖(公开:open)
|
|
137
|
-
app.get('/api/users/:id/auctions', (req, res) => {
|
|
138
|
-
const ownerId = resolveUserId(req.params.id);
|
|
129
|
+
app.get('/api/users/:id/auctions', async (req, res) => {
|
|
130
|
+
const ownerId = await resolveUserId(req.params.id);
|
|
139
131
|
if (!ownerId)
|
|
140
132
|
return void res.status(404).json({ error: 'user not found' });
|
|
141
|
-
const items =
|
|
133
|
+
const items = await dbAll(`
|
|
142
134
|
SELECT id, title, current_price, starting_price, status, deadline_at, bid_count, category, created_at
|
|
143
135
|
FROM auctions
|
|
144
136
|
WHERE seller_id = ? AND status = 'open' AND deadline_at > datetime('now')
|
|
145
137
|
ORDER BY deadline_at ASC LIMIT 50
|
|
146
|
-
|
|
138
|
+
`, [ownerId]);
|
|
147
139
|
res.json({ items });
|
|
148
140
|
});
|
|
149
141
|
// 用户写的测评(公开:作为买家给出的评价)
|
|
150
|
-
app.get('/api/users/:id/reviews', (req, res) => {
|
|
151
|
-
const ownerId = resolveUserId(req.params.id);
|
|
142
|
+
app.get('/api/users/:id/reviews', async (req, res) => {
|
|
143
|
+
const ownerId = await resolveUserId(req.params.id);
|
|
152
144
|
if (!ownerId)
|
|
153
145
|
return void res.status(404).json({ error: 'user not found' });
|
|
154
|
-
const items =
|
|
146
|
+
const items = await dbAll(`
|
|
155
147
|
SELECT r.order_id, r.product_id, r.stars, r.comment, r.reply, r.created_at,
|
|
156
148
|
p.title AS product_title, p.images AS product_images
|
|
157
149
|
FROM order_ratings r
|
|
158
150
|
LEFT JOIN products p ON p.id = r.product_id
|
|
159
151
|
WHERE r.buyer_id = ? AND (r.hidden_until IS NULL OR r.hidden_until <= datetime('now'))
|
|
160
152
|
ORDER BY r.created_at DESC LIMIT 50
|
|
161
|
-
|
|
153
|
+
`, [ownerId]);
|
|
162
154
|
res.json({ items });
|
|
163
155
|
});
|
|
164
156
|
// 用户在售商品(公开:卖家 active 商品)
|
|
165
|
-
app.get('/api/users/:id/products', (req, res) => {
|
|
166
|
-
const ownerId = resolveUserId(req.params.id);
|
|
157
|
+
app.get('/api/users/:id/products', async (req, res) => {
|
|
158
|
+
const ownerId = await resolveUserId(req.params.id);
|
|
167
159
|
if (!ownerId)
|
|
168
160
|
return void res.status(404).json({ error: 'user not found' });
|
|
169
|
-
const items =
|
|
161
|
+
const items = await dbAll(`
|
|
170
162
|
SELECT id, title, price, images, category, completion_count, total_likes, created_at
|
|
171
163
|
FROM products
|
|
172
164
|
WHERE seller_id = ? AND status = 'active' AND stock > 0
|
|
173
165
|
ORDER BY completion_count DESC, created_at DESC LIMIT 50
|
|
174
|
-
|
|
166
|
+
`, [ownerId]);
|
|
175
167
|
res.json({ items });
|
|
176
168
|
});
|
|
177
169
|
// 用户赞过的 shareables(仅 owner 可见)
|
|
178
|
-
app.get('/api/users/:id/liked-shareables', (req, res) => {
|
|
170
|
+
app.get('/api/users/:id/liked-shareables', async (req, res) => {
|
|
179
171
|
const me = auth(req, res);
|
|
180
172
|
if (!me)
|
|
181
173
|
return;
|
|
@@ -185,7 +177,7 @@ export function registerUsersPublicRoutes(app, deps) {
|
|
|
185
177
|
ownerId = me.id;
|
|
186
178
|
if (!ownerId)
|
|
187
179
|
return void res.status(403).json({ error: 'only owner can view liked list' });
|
|
188
|
-
const rows =
|
|
180
|
+
const rows = await dbAll(`
|
|
189
181
|
SELECT s.id, s.owner_id, s.owner_code, s.type, s.external_url, s.external_platform,
|
|
190
182
|
s.thumbnail_url, s.title, s.description, s.photo_hashes, s.related_product_id, s.related_anchor,
|
|
191
183
|
s.click_count, s.like_count, s.created_at,
|
|
@@ -196,7 +188,7 @@ export function registerUsersPublicRoutes(app, deps) {
|
|
|
196
188
|
LEFT JOIN products p ON p.id = s.related_product_id
|
|
197
189
|
WHERE l.user_id = ? AND s.status = 'active'
|
|
198
190
|
ORDER BY l.created_at DESC LIMIT 100
|
|
199
|
-
|
|
191
|
+
`, [ownerId]);
|
|
200
192
|
for (const r of rows) {
|
|
201
193
|
if (typeof r.photo_hashes === 'string') {
|
|
202
194
|
try {
|
|
@@ -210,20 +202,20 @@ export function registerUsersPublicRoutes(app, deps) {
|
|
|
210
202
|
res.json({ shareables: rows });
|
|
211
203
|
});
|
|
212
204
|
// 公开卡(未登录可调,分享 banner 用)
|
|
213
|
-
app.get('/api/users/:id/public-card', (req, res) => {
|
|
205
|
+
app.get('/api/users/:id/public-card', async (req, res) => {
|
|
214
206
|
const ref = String(req.params.id || '').trim();
|
|
215
207
|
const cols = "id, name, bio, search_anchor, created_at, permanent_code, handle";
|
|
216
208
|
const filter = " AND id != 'sys_protocol'";
|
|
217
209
|
let row;
|
|
218
210
|
if (/^usr_[A-Za-z0-9_]+$/.test(ref)) {
|
|
219
|
-
row =
|
|
211
|
+
row = await dbOne(`SELECT ${cols} FROM users WHERE id = ?${filter}`, [ref]);
|
|
220
212
|
}
|
|
221
213
|
if (!row && /^[A-Z0-9]{6,7}$/.test(ref.toUpperCase()) && !ref.startsWith('@')) {
|
|
222
|
-
row =
|
|
214
|
+
row = await dbOne(`SELECT ${cols} FROM users WHERE permanent_code = ?${filter}`, [ref.toUpperCase()]);
|
|
223
215
|
}
|
|
224
216
|
if (!row) {
|
|
225
217
|
const h = ref.replace(/^@/, '').toLowerCase();
|
|
226
|
-
row =
|
|
218
|
+
row = await dbOne(`SELECT ${cols} FROM users WHERE handle = ?${filter}`, [h]);
|
|
227
219
|
}
|
|
228
220
|
if (!row)
|
|
229
221
|
return void res.status(404).json({ error: 'not_found' });
|
|
@@ -240,7 +232,7 @@ export function registerUsersPublicRoutes(app, deps) {
|
|
|
240
232
|
});
|
|
241
233
|
});
|
|
242
234
|
// 公开用户主页 + D2 信誉徽章墙
|
|
243
|
-
app.get('/api/users/:user_id', (req, res) => {
|
|
235
|
+
app.get('/api/users/:user_id', async (req, res) => {
|
|
244
236
|
const me = auth(req, res);
|
|
245
237
|
if (!me)
|
|
246
238
|
return;
|
|
@@ -248,30 +240,30 @@ export function registerUsersPublicRoutes(app, deps) {
|
|
|
248
240
|
const cols = "id, name, role, region, bio, search_anchor, created_at, reputation, COALESCE(feed_visible, 1) as feed_visible";
|
|
249
241
|
let target;
|
|
250
242
|
if (/^usr_[A-Za-z0-9_]+$/.test(ref)) {
|
|
251
|
-
target =
|
|
243
|
+
target = await dbOne(`SELECT ${cols} FROM users WHERE id = ? AND id != 'sys_protocol'`, [ref]);
|
|
252
244
|
}
|
|
253
245
|
else if (/^[A-Z0-9]{6,7}$/.test(ref) && !ref.startsWith('@')) {
|
|
254
|
-
target =
|
|
246
|
+
target = await dbOne(`SELECT ${cols} FROM users WHERE permanent_code = ? AND id != 'sys_protocol'`, [ref.toUpperCase()]);
|
|
255
247
|
}
|
|
256
248
|
if (!target) {
|
|
257
249
|
const h = ref.replace(/^@/, '').toLowerCase();
|
|
258
|
-
target =
|
|
250
|
+
target = await dbOne(`SELECT ${cols} FROM users WHERE handle = ? AND id != 'sys_protocol'`, [h]);
|
|
259
251
|
}
|
|
260
252
|
if (!target)
|
|
261
253
|
return void res.status(404).json({ error: '用户不存在' });
|
|
262
|
-
const followers =
|
|
263
|
-
const following =
|
|
264
|
-
const isFollowing = !!
|
|
265
|
-
const purchaseCount =
|
|
266
|
-
const salesCount =
|
|
267
|
-
const likesReceived =
|
|
254
|
+
const followers = (await dbOne("SELECT COUNT(*) as n FROM follows WHERE followee_id = ?", [target.id])).n;
|
|
255
|
+
const following = (await dbOne("SELECT COUNT(*) as n FROM follows WHERE follower_id = ?", [target.id])).n;
|
|
256
|
+
const isFollowing = !!(await dbOne("SELECT 1 FROM follows WHERE follower_id = ? AND followee_id = ?", [me.id, target.id]));
|
|
257
|
+
const purchaseCount = (await dbOne("SELECT COUNT(*) as n FROM orders WHERE buyer_id = ? AND status = 'completed'", [target.id])).n;
|
|
258
|
+
const salesCount = (await dbOne("SELECT COUNT(*) as n FROM orders WHERE seller_id = ? AND status = 'completed'", [target.id])).n;
|
|
259
|
+
const likesReceived = (await dbOne("SELECT COALESCE(SUM(like_count), 0) as n FROM shareables WHERE owner_id = ? AND status = 'active'", [target.id])).n;
|
|
268
260
|
// 主人视角加私有统计
|
|
269
261
|
const isOwner = me.id === target.id;
|
|
270
262
|
let privateStats = null;
|
|
271
263
|
if (isOwner) {
|
|
272
|
-
const w =
|
|
273
|
-
const pv =
|
|
274
|
-
const score =
|
|
264
|
+
const w = await dbOne('SELECT balance, earned FROM wallets WHERE user_id = ?', [me.id]);
|
|
265
|
+
const pv = await dbOne("SELECT total_left_pv, total_right_pv FROM users WHERE id = ?", [me.id]);
|
|
266
|
+
const score = (await dbOne("SELECT COALESCE(SUM(score),0) as s FROM binary_score_records WHERE user_id = ? AND settled_at IS NULL", [me.id])).s;
|
|
275
267
|
privateStats = {
|
|
276
268
|
wallet_balance: Number(w?.balance ?? 0),
|
|
277
269
|
wallet_earned: Number(w?.earned ?? 0),
|
|
@@ -288,14 +280,14 @@ export function registerUsersPublicRoutes(app, deps) {
|
|
|
288
280
|
rep >= 30 ? { tier: 2, label: '可靠', emoji: '✓', color: '#16a34a' } :
|
|
289
281
|
{ tier: 1, label: '新手', emoji: '🌱', color: '#9ca3af' };
|
|
290
282
|
// Agent trust band(P1.2 隐私:trust_score 仅 owner 看,他人仅 level)
|
|
291
|
-
const agentRow =
|
|
283
|
+
const agentRow = await dbOne(`SELECT level, MAX(trust_score) as score FROM agent_reputation WHERE user_id = ? GROUP BY user_id`, [target.id]);
|
|
292
284
|
const agentBand = agentRow ? (isOwner
|
|
293
285
|
? { level: agentRow.level, score: Math.round(agentRow.score || 0) }
|
|
294
286
|
: { level: agentRow.level }) : null;
|
|
295
|
-
const charity =
|
|
287
|
+
const charity = await dbOne(`SELECT prestige_score, badge_tier, wishes_fulfilled, wishes_made FROM charity_reputation WHERE user_id = ?`, [target.id]);
|
|
296
288
|
let verifier;
|
|
297
289
|
try {
|
|
298
|
-
verifier =
|
|
290
|
+
verifier = await dbOne(`SELECT tier FROM verifier_whitelist WHERE user_id = ?`, [target.id]);
|
|
299
291
|
}
|
|
300
292
|
catch { }
|
|
301
293
|
res.json({
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
/** options → canonical key(sort + join)— 防同规格组合两次入库 */
|
|
2
3
|
function canonicalOptionsKey(options) {
|
|
3
4
|
return Object.keys(options).sort().map(k => `${k}=${String(options[k])}`).join('|');
|
|
@@ -5,13 +6,13 @@ function canonicalOptionsKey(options) {
|
|
|
5
6
|
export function registerVariantsRoutes(app, deps) {
|
|
6
7
|
const { db, generateId, auth } = deps;
|
|
7
8
|
// 公开列出(含 buyer 下单页查可选项)
|
|
8
|
-
app.get('/api/products/:product_id/variants', (req, res) => {
|
|
9
|
-
const rows =
|
|
9
|
+
app.get('/api/products/:product_id/variants', async (req, res) => {
|
|
10
|
+
const rows = await dbAll(`
|
|
10
11
|
SELECT id, sku, options_json, price_override, stock, images_json, is_active, created_at
|
|
11
12
|
FROM product_variants
|
|
12
13
|
WHERE product_id = ? AND is_active = 1
|
|
13
14
|
ORDER BY created_at ASC LIMIT 100
|
|
14
|
-
|
|
15
|
+
`, [req.params.product_id]);
|
|
15
16
|
const items = rows.map(r => {
|
|
16
17
|
let options = {};
|
|
17
18
|
let images = [];
|
|
@@ -28,11 +29,11 @@ export function registerVariantsRoutes(app, deps) {
|
|
|
28
29
|
});
|
|
29
30
|
res.json({ items });
|
|
30
31
|
});
|
|
31
|
-
app.post('/api/products/:product_id/variants', (req, res) => {
|
|
32
|
+
app.post('/api/products/:product_id/variants', async (req, res) => {
|
|
32
33
|
const user = auth(req, res);
|
|
33
34
|
if (!user)
|
|
34
35
|
return;
|
|
35
|
-
const p =
|
|
36
|
+
const p = await dbOne('SELECT id, seller_id FROM products WHERE id = ?', [req.params.product_id]);
|
|
36
37
|
if (!p)
|
|
37
38
|
return void res.status(404).json({ error: '商品不存在' });
|
|
38
39
|
if (p.seller_id !== user.id)
|
|
@@ -46,8 +47,7 @@ export function registerVariantsRoutes(app, deps) {
|
|
|
46
47
|
if (priceN != null && (priceN <= 0 || priceN > 1_000_000))
|
|
47
48
|
return void res.status(400).json({ error: 'price_override 无效' });
|
|
48
49
|
const optKey = canonicalOptionsKey(options);
|
|
49
|
-
const dup =
|
|
50
|
-
.get(req.params.product_id, optKey);
|
|
50
|
+
const dup = await dbOne(`SELECT id FROM product_variants WHERE product_id = ? AND options_key = ? AND is_active = 1`, [req.params.product_id, optKey]);
|
|
51
51
|
if (dup)
|
|
52
52
|
return void res.status(409).json({ error: '该规格组合已存在', existing_id: dup.id });
|
|
53
53
|
const id = generateId('pv');
|
|
@@ -66,11 +66,11 @@ export function registerVariantsRoutes(app, deps) {
|
|
|
66
66
|
})();
|
|
67
67
|
res.json({ success: true, id });
|
|
68
68
|
});
|
|
69
|
-
app.patch('/api/products/:product_id/variants/:variant_id', (req, res) => {
|
|
69
|
+
app.patch('/api/products/:product_id/variants/:variant_id', async (req, res) => {
|
|
70
70
|
const user = auth(req, res);
|
|
71
71
|
if (!user)
|
|
72
72
|
return;
|
|
73
|
-
const v =
|
|
73
|
+
const v = await dbOne(`SELECT v.*, p.seller_id FROM product_variants v JOIN products p ON p.id = v.product_id WHERE v.id = ? AND v.product_id = ?`, [req.params.variant_id, req.params.product_id]);
|
|
74
74
|
if (!v)
|
|
75
75
|
return void res.status(404).json({ error: 'variant 不存在' });
|
|
76
76
|
if (v.seller_id !== user.id)
|
|
@@ -87,8 +87,7 @@ export function registerVariantsRoutes(app, deps) {
|
|
|
87
87
|
return void res.status(400).json({ error: 'options 不能为空' });
|
|
88
88
|
}
|
|
89
89
|
const newKey = canonicalOptionsKey(options);
|
|
90
|
-
const dup =
|
|
91
|
-
.get(req.params.product_id, newKey, req.params.variant_id);
|
|
90
|
+
const dup = await dbOne(`SELECT id FROM product_variants WHERE product_id = ? AND options_key = ? AND id != ? AND is_active = 1`, [req.params.product_id, newKey, req.params.variant_id]);
|
|
92
91
|
if (dup)
|
|
93
92
|
return void res.status(409).json({ error: '该规格组合已存在', existing_id: dup.id });
|
|
94
93
|
sets.push('options_json = ?');
|
|
@@ -129,11 +128,11 @@ export function registerVariantsRoutes(app, deps) {
|
|
|
129
128
|
})();
|
|
130
129
|
res.json({ success: true });
|
|
131
130
|
});
|
|
132
|
-
app.delete('/api/products/:product_id/variants/:variant_id', (req, res) => {
|
|
131
|
+
app.delete('/api/products/:product_id/variants/:variant_id', async (req, res) => {
|
|
133
132
|
const user = auth(req, res);
|
|
134
133
|
if (!user)
|
|
135
134
|
return;
|
|
136
|
-
const v =
|
|
135
|
+
const v = await dbOne(`SELECT v.id, v.stock, p.seller_id FROM product_variants v JOIN products p ON p.id = v.product_id WHERE v.id = ? AND v.product_id = ?`, [req.params.variant_id, req.params.product_id]);
|
|
137
136
|
if (!v)
|
|
138
137
|
return void res.status(404).json({ error: 'variant 不存在' });
|
|
139
138
|
if (v.seller_id !== user.id)
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { dbOne, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam (dbRun: appeal single-write)
|
|
1
2
|
export function registerVerifierUserRoutes(app, deps) {
|
|
3
|
+
// 只读/单写站点走 RFC-016 异步 seam;db 保留:apply/withdraw 是 stake 资金路径,
|
|
4
|
+
// 状态翻转 + 钱包扣/退必须原子(db.transaction + CAS),Phase 3 迁 pg 行锁。
|
|
2
5
|
const { db, generateId, auth, errorRes, checkVerifierEligibility, getVerifierState, resetDailyQuotaIfNeeded, TIER_QUOTAS, VERIFIER_STAKE_REQUIRED, APP_REJECT_COOLDOWN_DAYS, } = deps;
|
|
3
6
|
app.get('/api/verifier/eligibility', (req, res) => {
|
|
4
7
|
const user = auth(req, res);
|
|
@@ -26,7 +29,7 @@ export function registerVerifierUserRoutes(app, deps) {
|
|
|
26
29
|
stake_amount: Number(wl?.stake_amount ?? 0),
|
|
27
30
|
});
|
|
28
31
|
});
|
|
29
|
-
app.post('/api/verifier/apply', (req, res) => {
|
|
32
|
+
app.post('/api/verifier/apply', async (req, res) => {
|
|
30
33
|
const user = auth(req, res);
|
|
31
34
|
if (!user)
|
|
32
35
|
return;
|
|
@@ -35,14 +38,14 @@ export function registerVerifierUserRoutes(app, deps) {
|
|
|
35
38
|
if (user.role !== 'buyer') {
|
|
36
39
|
return void errorRes(res, 403, 'ROLE_NOT_BUYER', '外部审核员仅 buyer 角色可申请(卖家 / 受信角色请联系管理员)');
|
|
37
40
|
}
|
|
38
|
-
const wl =
|
|
41
|
+
const wl = await dbOne("SELECT 1 FROM verifier_whitelist WHERE user_id = ?", [userId]);
|
|
39
42
|
if (wl)
|
|
40
43
|
return void res.json({ error: '你已经是审核员,无需重新申请' });
|
|
41
|
-
const pending =
|
|
44
|
+
const pending = await dbOne("SELECT 1 FROM verifier_applications WHERE user_id = ? AND status = 'pending'", [userId]);
|
|
42
45
|
if (pending)
|
|
43
46
|
return void res.json({ error: '你已有待审申请' });
|
|
44
47
|
// 30d 拒绝冷却
|
|
45
|
-
const lastReject =
|
|
48
|
+
const lastReject = await dbOne("SELECT reviewed_at FROM verifier_applications WHERE user_id = ? AND status = 'rejected' ORDER BY reviewed_at DESC LIMIT 1", [userId]);
|
|
46
49
|
if (lastReject?.reviewed_at) {
|
|
47
50
|
const cooldownEnd = new Date(new Date(lastReject.reviewed_at).getTime() + APP_REJECT_COOLDOWN_DAYS * 86400_000);
|
|
48
51
|
if (cooldownEnd > new Date()) {
|
|
@@ -53,34 +56,72 @@ export function registerVerifierUserRoutes(app, deps) {
|
|
|
53
56
|
if (!elig.eligible) {
|
|
54
57
|
return void res.json({ error: '信誉指标未达标', eligibility: elig });
|
|
55
58
|
}
|
|
59
|
+
// 友好预检查(读):真正的守恒门在事务内(WHERE balance >= stake)。
|
|
56
60
|
if (VERIFIER_STAKE_REQUIRED > 0) {
|
|
57
|
-
const wallet =
|
|
61
|
+
const wallet = await dbOne("SELECT balance FROM wallets WHERE user_id = ?", [userId]);
|
|
58
62
|
if (!wallet || wallet.balance < VERIFIER_STAKE_REQUIRED) {
|
|
59
63
|
return void res.json({ error: `质押需 ${VERIFIER_STAKE_REQUIRED} WAZ,钱包余额不足` });
|
|
60
64
|
}
|
|
61
|
-
db.prepare("UPDATE wallets SET balance = balance - ?, staked = staked + ? WHERE user_id = ?")
|
|
62
|
-
.run(VERIFIER_STAKE_REQUIRED, VERIFIER_STAKE_REQUIRED, userId);
|
|
63
65
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
const appId = generateId('vapp');
|
|
67
|
+
// stake 原子段:重检 whitelist/pending(防并发双申请双质押)+ 钱包扣减(守恒 guard)+ INSERT 申请
|
|
68
|
+
try {
|
|
69
|
+
db.transaction(() => {
|
|
70
|
+
if (db.prepare("SELECT 1 FROM verifier_whitelist WHERE user_id = ?").get(userId))
|
|
71
|
+
throw new Error('VER_ALREADY');
|
|
72
|
+
if (db.prepare("SELECT 1 FROM verifier_applications WHERE user_id = ? AND status = 'pending'").get(userId))
|
|
73
|
+
throw new Error('VER_PENDING');
|
|
74
|
+
if (VERIFIER_STAKE_REQUIRED > 0) {
|
|
75
|
+
const debit = db.prepare("UPDATE wallets SET balance = balance - ?, staked = staked + ? WHERE user_id = ? AND balance >= ?")
|
|
76
|
+
.run(VERIFIER_STAKE_REQUIRED, VERIFIER_STAKE_REQUIRED, userId, VERIFIER_STAKE_REQUIRED);
|
|
77
|
+
if (debit.changes === 0)
|
|
78
|
+
throw new Error('VER_INSUFFICIENT');
|
|
79
|
+
}
|
|
80
|
+
db.prepare(`INSERT INTO verifier_applications (id, user_id, status, snapshot) VALUES (?,?,?,?)`)
|
|
81
|
+
.run(appId, userId, 'pending', JSON.stringify(elig.items));
|
|
82
|
+
})();
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
const msg = e.message;
|
|
86
|
+
if (msg === 'VER_ALREADY')
|
|
87
|
+
return void res.json({ error: '你已经是审核员,无需重新申请' });
|
|
88
|
+
if (msg === 'VER_PENDING')
|
|
89
|
+
return void res.json({ error: '你已有待审申请' });
|
|
90
|
+
if (msg === 'VER_INSUFFICIENT')
|
|
91
|
+
return void res.json({ error: `质押需 ${VERIFIER_STAKE_REQUIRED} WAZ,钱包余额不足` });
|
|
92
|
+
console.error('[verifier apply tx]', msg);
|
|
93
|
+
return void res.status(500).json({ error: '申请失败,请重试' });
|
|
94
|
+
}
|
|
66
95
|
res.json({ success: true, stake_locked: VERIFIER_STAKE_REQUIRED });
|
|
67
96
|
});
|
|
68
|
-
app.post('/api/verifier/withdraw-application', (req, res) => {
|
|
97
|
+
app.post('/api/verifier/withdraw-application', async (req, res) => {
|
|
69
98
|
const user = auth(req, res);
|
|
70
99
|
if (!user)
|
|
71
100
|
return;
|
|
72
101
|
const userId = user.id;
|
|
73
|
-
const pending =
|
|
102
|
+
const pending = await dbOne("SELECT id FROM verifier_applications WHERE user_id = ? AND status = 'pending' LIMIT 1", [userId]);
|
|
74
103
|
if (!pending)
|
|
75
104
|
return void res.json({ error: '没有待审申请' });
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
db.
|
|
79
|
-
.
|
|
105
|
+
// 原子段:CAS 翻转 pending→withdrawn(防并发/admin 抢跑双退)+ 退质押仅在本请求真翻转时
|
|
106
|
+
try {
|
|
107
|
+
db.transaction(() => {
|
|
108
|
+
const cas = db.prepare("UPDATE verifier_applications SET status = 'withdrawn', reviewed_at = datetime('now') WHERE id = ? AND status = 'pending'").run(pending.id);
|
|
109
|
+
if (cas.changes === 0)
|
|
110
|
+
throw new Error('VER_RACE');
|
|
111
|
+
if (VERIFIER_STAKE_REQUIRED > 0) {
|
|
112
|
+
db.prepare("UPDATE wallets SET balance = balance + ?, staked = staked - ? WHERE user_id = ?").run(VERIFIER_STAKE_REQUIRED, VERIFIER_STAKE_REQUIRED, userId);
|
|
113
|
+
}
|
|
114
|
+
})();
|
|
115
|
+
}
|
|
116
|
+
catch (e) {
|
|
117
|
+
if (e.message === 'VER_RACE')
|
|
118
|
+
return void res.status(409).json({ error: '申请状态已变化,请刷新' });
|
|
119
|
+
console.error('[verifier withdraw tx]', e.message);
|
|
120
|
+
return void res.status(500).json({ error: '撤回失败,请重试' });
|
|
80
121
|
}
|
|
81
122
|
res.json({ success: true });
|
|
82
123
|
});
|
|
83
|
-
app.post('/api/verifier/appeal', (req, res) => {
|
|
124
|
+
app.post('/api/verifier/appeal', async (req, res) => {
|
|
84
125
|
const user = auth(req, res);
|
|
85
126
|
if (!user)
|
|
86
127
|
return;
|
|
@@ -90,18 +131,17 @@ export function registerVerifierUserRoutes(app, deps) {
|
|
|
90
131
|
if (reason.length > 500)
|
|
91
132
|
return void res.json({ error: '申诉理由过长(>500 字)' });
|
|
92
133
|
// 必须当前 suspended
|
|
93
|
-
const stats =
|
|
134
|
+
const stats = await dbOne("SELECT suspended_until FROM verifier_stats WHERE user_id = ?", [user.id]);
|
|
94
135
|
if (!stats?.suspended_until || new Date(stats.suspended_until).getTime() <= Date.now()) {
|
|
95
136
|
return void res.json({ error: '当前未处于暂停状态,无需申诉' });
|
|
96
137
|
}
|
|
97
138
|
// 近 30 天有申诉过即重复
|
|
98
|
-
const recent =
|
|
139
|
+
const recent = await dbOne(`SELECT 1 FROM verifier_appeals WHERE user_id = ? AND created_at > datetime('now','-30 day')`, [user.id]);
|
|
99
140
|
if (recent)
|
|
100
141
|
return void res.json({ error: '近期已申诉过,每次处罚只能申诉一次' });
|
|
101
142
|
const evidenceArr = Array.isArray(evidence_urls) ? evidence_urls.slice(0, 3) : [];
|
|
102
|
-
|
|
103
|
-
VALUES (?,?,?,?,?,?)
|
|
104
|
-
.run(generateId('vapl'), user.id, task_id || null, submission_id || null, reason.trim(), JSON.stringify(evidenceArr));
|
|
143
|
+
await dbRun(`INSERT INTO verifier_appeals (id, user_id, task_id, submission_id, reason, evidence_urls)
|
|
144
|
+
VALUES (?,?,?,?,?,?)`, [generateId('vapl'), user.id, task_id || null, submission_id || null, reason.trim(), JSON.stringify(evidenceArr)]);
|
|
105
145
|
res.json({ success: true });
|
|
106
146
|
});
|
|
107
147
|
}
|