@seasonkoh/webaz 0.1.24 → 0.1.25
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 +2 -0
- 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 +165 -64
- 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 +173 -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 +10 -2
- 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-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-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/contract-fingerprint.js +2 -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 +575 -68
- package/dist/pwa/public/i18n.js +29 -20
- package/dist/pwa/public/index.html +1 -0
- package/dist/pwa/public/openapi.json +2 -2
- 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-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 +26 -29
- package/dist/pwa/routes/admin-ops.js +22 -21
- 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 +54 -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 +7 -5
- 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 +153 -114
- package/dist/pwa/routes/auth-login.js +6 -4
- package/dist/pwa/routes/auth-read.js +11 -9
- package/dist/pwa/routes/auth-register.js +35 -20
- 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 +147 -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 +33 -30
- 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 +33 -17
- 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 +14 -16
- 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 +19 -17
- package/dist/pwa/routes/profile-prefs.js +11 -11
- package/dist/pwa/routes/promoter.js +55 -49
- 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 +30 -30
- package/dist/pwa/routes/recover-key.js +13 -12
- package/dist/pwa/routes/referral.js +37 -32
- 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 +59 -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 +57 -0
- package/dist/pwa/routes/shops.js +20 -18
- 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 +45 -0
- package/dist/pwa/routes/trial.js +69 -51
- 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 -60
- 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 +74 -36
- 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 +237 -81
- package/dist/version.js +1 -1
- package/package.json +47 -2
|
@@ -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,29 +44,29 @@ 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 =
|
|
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;
|
|
63
|
+
const score = (await dbOne(`
|
|
62
64
|
SELECT
|
|
63
65
|
COALESCE(SUM(CASE WHEN settled_at IS NULL THEN score ELSE 0 END),0) AS pending_score,
|
|
64
66
|
COALESCE(SUM(CASE WHEN settled_at IS NOT NULL THEN waz_amount ELSE 0 END),0) AS settled_waz,
|
|
65
67
|
COUNT(*) AS total_hits
|
|
66
68
|
FROM binary_score_records WHERE user_id = ?
|
|
67
|
-
|
|
69
|
+
`, [targetId]));
|
|
68
70
|
const leftPv = Number(u.total_left_pv ?? 0);
|
|
69
71
|
const rightPv = Number(u.total_right_pv ?? 0);
|
|
70
72
|
const weak = Math.min(leftPv, rightPv);
|
|
@@ -86,25 +88,25 @@ export function registerUsersPublicRoutes(app, deps) {
|
|
|
86
88
|
});
|
|
87
89
|
});
|
|
88
90
|
// 用户公开 shareables
|
|
89
|
-
app.get('/api/users/:id/shareables', (req, res) => {
|
|
91
|
+
app.get('/api/users/:id/shareables', async (req, res) => {
|
|
90
92
|
const ref = String(req.params.id || '').trim();
|
|
91
93
|
let ownerId = null;
|
|
92
94
|
if (/^usr_[A-Za-z0-9_]+$/.test(ref))
|
|
93
95
|
ownerId = ref;
|
|
94
96
|
else if (/^[A-Z0-9]{6,7}$/i.test(ref) && !ref.startsWith('@')) {
|
|
95
|
-
const r =
|
|
97
|
+
const r = await dbOne("SELECT id FROM users WHERE permanent_code = ?", [ref.toUpperCase()]);
|
|
96
98
|
if (r)
|
|
97
99
|
ownerId = r.id;
|
|
98
100
|
}
|
|
99
101
|
if (!ownerId) {
|
|
100
102
|
const h = ref.replace(/^@/, '').toLowerCase();
|
|
101
|
-
const r =
|
|
103
|
+
const r = await dbOne("SELECT id FROM users WHERE handle = ?", [h]);
|
|
102
104
|
if (r)
|
|
103
105
|
ownerId = r.id;
|
|
104
106
|
}
|
|
105
107
|
if (!ownerId)
|
|
106
108
|
return void res.status(404).json({ error: 'user not found' });
|
|
107
|
-
const rows =
|
|
109
|
+
const rows = await dbAll(`
|
|
108
110
|
SELECT s.id, s.owner_id, s.owner_code, s.type, s.external_url, s.external_platform, s.external_video_id,
|
|
109
111
|
s.thumbnail_url, s.title, s.description, s.related_product_id, s.related_anchor,
|
|
110
112
|
s.related_order_id, s.photo_hashes,
|
|
@@ -114,68 +116,68 @@ export function registerUsersPublicRoutes(app, deps) {
|
|
|
114
116
|
LEFT JOIN products p ON p.id = s.related_product_id
|
|
115
117
|
WHERE s.owner_id = ? AND s.status = 'active'
|
|
116
118
|
ORDER BY s.created_at DESC LIMIT 100
|
|
117
|
-
|
|
119
|
+
`, [ownerId]);
|
|
118
120
|
for (const r of rows) {
|
|
119
121
|
r.badges = noteAuthenticityBadges(r);
|
|
120
122
|
}
|
|
121
123
|
res.json({ shareables: rows });
|
|
122
124
|
});
|
|
123
125
|
// 用户在售二手(公开:available + reserved)
|
|
124
|
-
app.get('/api/users/:id/secondhand', (req, res) => {
|
|
125
|
-
const ownerId = resolveUserId(req.params.id);
|
|
126
|
+
app.get('/api/users/:id/secondhand', async (req, res) => {
|
|
127
|
+
const ownerId = await resolveUserId(req.params.id);
|
|
126
128
|
if (!ownerId)
|
|
127
129
|
return void res.status(404).json({ error: 'user not found' });
|
|
128
|
-
const items =
|
|
130
|
+
const items = await dbAll(`
|
|
129
131
|
SELECT id, title, price, condition_grade, images, status, category, created_at
|
|
130
132
|
FROM secondhand_items
|
|
131
133
|
WHERE seller_id = ? AND status IN ('available', 'reserved')
|
|
132
134
|
ORDER BY created_at DESC LIMIT 50
|
|
133
|
-
|
|
135
|
+
`, [ownerId]);
|
|
134
136
|
res.json({ items });
|
|
135
137
|
});
|
|
136
138
|
// 用户进行中拍卖(公开:open)
|
|
137
|
-
app.get('/api/users/:id/auctions', (req, res) => {
|
|
138
|
-
const ownerId = resolveUserId(req.params.id);
|
|
139
|
+
app.get('/api/users/:id/auctions', async (req, res) => {
|
|
140
|
+
const ownerId = await resolveUserId(req.params.id);
|
|
139
141
|
if (!ownerId)
|
|
140
142
|
return void res.status(404).json({ error: 'user not found' });
|
|
141
|
-
const items =
|
|
143
|
+
const items = await dbAll(`
|
|
142
144
|
SELECT id, title, current_price, starting_price, status, deadline_at, bid_count, category, created_at
|
|
143
145
|
FROM auctions
|
|
144
146
|
WHERE seller_id = ? AND status = 'open' AND deadline_at > datetime('now')
|
|
145
147
|
ORDER BY deadline_at ASC LIMIT 50
|
|
146
|
-
|
|
148
|
+
`, [ownerId]);
|
|
147
149
|
res.json({ items });
|
|
148
150
|
});
|
|
149
151
|
// 用户写的测评(公开:作为买家给出的评价)
|
|
150
|
-
app.get('/api/users/:id/reviews', (req, res) => {
|
|
151
|
-
const ownerId = resolveUserId(req.params.id);
|
|
152
|
+
app.get('/api/users/:id/reviews', async (req, res) => {
|
|
153
|
+
const ownerId = await resolveUserId(req.params.id);
|
|
152
154
|
if (!ownerId)
|
|
153
155
|
return void res.status(404).json({ error: 'user not found' });
|
|
154
|
-
const items =
|
|
156
|
+
const items = await dbAll(`
|
|
155
157
|
SELECT r.order_id, r.product_id, r.stars, r.comment, r.reply, r.created_at,
|
|
156
158
|
p.title AS product_title, p.images AS product_images
|
|
157
159
|
FROM order_ratings r
|
|
158
160
|
LEFT JOIN products p ON p.id = r.product_id
|
|
159
161
|
WHERE r.buyer_id = ? AND (r.hidden_until IS NULL OR r.hidden_until <= datetime('now'))
|
|
160
162
|
ORDER BY r.created_at DESC LIMIT 50
|
|
161
|
-
|
|
163
|
+
`, [ownerId]);
|
|
162
164
|
res.json({ items });
|
|
163
165
|
});
|
|
164
166
|
// 用户在售商品(公开:卖家 active 商品)
|
|
165
|
-
app.get('/api/users/:id/products', (req, res) => {
|
|
166
|
-
const ownerId = resolveUserId(req.params.id);
|
|
167
|
+
app.get('/api/users/:id/products', async (req, res) => {
|
|
168
|
+
const ownerId = await resolveUserId(req.params.id);
|
|
167
169
|
if (!ownerId)
|
|
168
170
|
return void res.status(404).json({ error: 'user not found' });
|
|
169
|
-
const items =
|
|
171
|
+
const items = await dbAll(`
|
|
170
172
|
SELECT id, title, price, images, category, completion_count, total_likes, created_at
|
|
171
173
|
FROM products
|
|
172
174
|
WHERE seller_id = ? AND status = 'active' AND stock > 0
|
|
173
175
|
ORDER BY completion_count DESC, created_at DESC LIMIT 50
|
|
174
|
-
|
|
176
|
+
`, [ownerId]);
|
|
175
177
|
res.json({ items });
|
|
176
178
|
});
|
|
177
179
|
// 用户赞过的 shareables(仅 owner 可见)
|
|
178
|
-
app.get('/api/users/:id/liked-shareables', (req, res) => {
|
|
180
|
+
app.get('/api/users/:id/liked-shareables', async (req, res) => {
|
|
179
181
|
const me = auth(req, res);
|
|
180
182
|
if (!me)
|
|
181
183
|
return;
|
|
@@ -185,7 +187,7 @@ export function registerUsersPublicRoutes(app, deps) {
|
|
|
185
187
|
ownerId = me.id;
|
|
186
188
|
if (!ownerId)
|
|
187
189
|
return void res.status(403).json({ error: 'only owner can view liked list' });
|
|
188
|
-
const rows =
|
|
190
|
+
const rows = await dbAll(`
|
|
189
191
|
SELECT s.id, s.owner_id, s.owner_code, s.type, s.external_url, s.external_platform,
|
|
190
192
|
s.thumbnail_url, s.title, s.description, s.photo_hashes, s.related_product_id, s.related_anchor,
|
|
191
193
|
s.click_count, s.like_count, s.created_at,
|
|
@@ -196,7 +198,7 @@ export function registerUsersPublicRoutes(app, deps) {
|
|
|
196
198
|
LEFT JOIN products p ON p.id = s.related_product_id
|
|
197
199
|
WHERE l.user_id = ? AND s.status = 'active'
|
|
198
200
|
ORDER BY l.created_at DESC LIMIT 100
|
|
199
|
-
|
|
201
|
+
`, [ownerId]);
|
|
200
202
|
for (const r of rows) {
|
|
201
203
|
if (typeof r.photo_hashes === 'string') {
|
|
202
204
|
try {
|
|
@@ -210,20 +212,20 @@ export function registerUsersPublicRoutes(app, deps) {
|
|
|
210
212
|
res.json({ shareables: rows });
|
|
211
213
|
});
|
|
212
214
|
// 公开卡(未登录可调,分享 banner 用)
|
|
213
|
-
app.get('/api/users/:id/public-card', (req, res) => {
|
|
215
|
+
app.get('/api/users/:id/public-card', async (req, res) => {
|
|
214
216
|
const ref = String(req.params.id || '').trim();
|
|
215
217
|
const cols = "id, name, bio, search_anchor, created_at, permanent_code, handle";
|
|
216
218
|
const filter = " AND id != 'sys_protocol'";
|
|
217
219
|
let row;
|
|
218
220
|
if (/^usr_[A-Za-z0-9_]+$/.test(ref)) {
|
|
219
|
-
row =
|
|
221
|
+
row = await dbOne(`SELECT ${cols} FROM users WHERE id = ?${filter}`, [ref]);
|
|
220
222
|
}
|
|
221
223
|
if (!row && /^[A-Z0-9]{6,7}$/.test(ref.toUpperCase()) && !ref.startsWith('@')) {
|
|
222
|
-
row =
|
|
224
|
+
row = await dbOne(`SELECT ${cols} FROM users WHERE permanent_code = ?${filter}`, [ref.toUpperCase()]);
|
|
223
225
|
}
|
|
224
226
|
if (!row) {
|
|
225
227
|
const h = ref.replace(/^@/, '').toLowerCase();
|
|
226
|
-
row =
|
|
228
|
+
row = await dbOne(`SELECT ${cols} FROM users WHERE handle = ?${filter}`, [h]);
|
|
227
229
|
}
|
|
228
230
|
if (!row)
|
|
229
231
|
return void res.status(404).json({ error: 'not_found' });
|
|
@@ -240,7 +242,7 @@ export function registerUsersPublicRoutes(app, deps) {
|
|
|
240
242
|
});
|
|
241
243
|
});
|
|
242
244
|
// 公开用户主页 + D2 信誉徽章墙
|
|
243
|
-
app.get('/api/users/:user_id', (req, res) => {
|
|
245
|
+
app.get('/api/users/:user_id', async (req, res) => {
|
|
244
246
|
const me = auth(req, res);
|
|
245
247
|
if (!me)
|
|
246
248
|
return;
|
|
@@ -248,30 +250,30 @@ export function registerUsersPublicRoutes(app, deps) {
|
|
|
248
250
|
const cols = "id, name, role, region, bio, search_anchor, created_at, reputation, COALESCE(feed_visible, 1) as feed_visible";
|
|
249
251
|
let target;
|
|
250
252
|
if (/^usr_[A-Za-z0-9_]+$/.test(ref)) {
|
|
251
|
-
target =
|
|
253
|
+
target = await dbOne(`SELECT ${cols} FROM users WHERE id = ? AND id != 'sys_protocol'`, [ref]);
|
|
252
254
|
}
|
|
253
255
|
else if (/^[A-Z0-9]{6,7}$/.test(ref) && !ref.startsWith('@')) {
|
|
254
|
-
target =
|
|
256
|
+
target = await dbOne(`SELECT ${cols} FROM users WHERE permanent_code = ? AND id != 'sys_protocol'`, [ref.toUpperCase()]);
|
|
255
257
|
}
|
|
256
258
|
if (!target) {
|
|
257
259
|
const h = ref.replace(/^@/, '').toLowerCase();
|
|
258
|
-
target =
|
|
260
|
+
target = await dbOne(`SELECT ${cols} FROM users WHERE handle = ? AND id != 'sys_protocol'`, [h]);
|
|
259
261
|
}
|
|
260
262
|
if (!target)
|
|
261
263
|
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 =
|
|
264
|
+
const followers = (await dbOne("SELECT COUNT(*) as n FROM follows WHERE followee_id = ?", [target.id])).n;
|
|
265
|
+
const following = (await dbOne("SELECT COUNT(*) as n FROM follows WHERE follower_id = ?", [target.id])).n;
|
|
266
|
+
const isFollowing = !!(await dbOne("SELECT 1 FROM follows WHERE follower_id = ? AND followee_id = ?", [me.id, target.id]));
|
|
267
|
+
const purchaseCount = (await dbOne("SELECT COUNT(*) as n FROM orders WHERE buyer_id = ? AND status = 'completed'", [target.id])).n;
|
|
268
|
+
const salesCount = (await dbOne("SELECT COUNT(*) as n FROM orders WHERE seller_id = ? AND status = 'completed'", [target.id])).n;
|
|
269
|
+
const likesReceived = (await dbOne("SELECT COALESCE(SUM(like_count), 0) as n FROM shareables WHERE owner_id = ? AND status = 'active'", [target.id])).n;
|
|
268
270
|
// 主人视角加私有统计
|
|
269
271
|
const isOwner = me.id === target.id;
|
|
270
272
|
let privateStats = null;
|
|
271
273
|
if (isOwner) {
|
|
272
|
-
const w =
|
|
273
|
-
const pv =
|
|
274
|
-
const score =
|
|
274
|
+
const w = await dbOne('SELECT balance, earned FROM wallets WHERE user_id = ?', [me.id]);
|
|
275
|
+
const pv = await dbOne("SELECT total_left_pv, total_right_pv FROM users WHERE id = ?", [me.id]);
|
|
276
|
+
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
277
|
privateStats = {
|
|
276
278
|
wallet_balance: Number(w?.balance ?? 0),
|
|
277
279
|
wallet_earned: Number(w?.earned ?? 0),
|
|
@@ -288,14 +290,14 @@ export function registerUsersPublicRoutes(app, deps) {
|
|
|
288
290
|
rep >= 30 ? { tier: 2, label: '可靠', emoji: '✓', color: '#16a34a' } :
|
|
289
291
|
{ tier: 1, label: '新手', emoji: '🌱', color: '#9ca3af' };
|
|
290
292
|
// Agent trust band(P1.2 隐私:trust_score 仅 owner 看,他人仅 level)
|
|
291
|
-
const agentRow =
|
|
293
|
+
const agentRow = await dbOne(`SELECT level, MAX(trust_score) as score FROM agent_reputation WHERE user_id = ? GROUP BY user_id`, [target.id]);
|
|
292
294
|
const agentBand = agentRow ? (isOwner
|
|
293
295
|
? { level: agentRow.level, score: Math.round(agentRow.score || 0) }
|
|
294
296
|
: { level: agentRow.level }) : null;
|
|
295
|
-
const charity =
|
|
297
|
+
const charity = await dbOne(`SELECT prestige_score, badge_tier, wishes_fulfilled, wishes_made FROM charity_reputation WHERE user_id = ?`, [target.id]);
|
|
296
298
|
let verifier;
|
|
297
299
|
try {
|
|
298
|
-
verifier =
|
|
300
|
+
verifier = await dbOne(`SELECT tier FROM verifier_whitelist WHERE user_id = ?`, [target.id]);
|
|
299
301
|
}
|
|
300
302
|
catch { }
|
|
301
303
|
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
|
}
|