@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
package/dist/pwa/routes/trial.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
// RFC-016 Phase 1 — 端点纯校验读/公开读/读回 + 单语句写 + cron 顶层扫描读 → async seam;
|
|
2
|
+
// 退款 db.transaction + claim 抢名额 tx + cron 逐 claim 评估读写保持同步(Phase 3 迁 pg)。
|
|
3
|
+
import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js';
|
|
1
4
|
// ─── 评估 cron — 重算 reach_score 决定退款 / 兜底超时 ──────────
|
|
2
5
|
// reach_score = views(unique_click_count)*0.1 + shares(child notes)*1 + conversions(attributed orders)*10
|
|
3
6
|
// 达阈 → 从卖家钱包扣 refund_amount 退给买家;30 天兜底超时
|
|
4
|
-
export function evaluateTrialClaims(db, generateId) {
|
|
7
|
+
export async function evaluateTrialClaims(db, generateId) {
|
|
5
8
|
let evaluated = 0, refunded = 0, expired = 0;
|
|
6
|
-
|
|
9
|
+
// RFC-016: 顶层候选扫描读 → seam;下方逐 claim 的 metrics 读 + 退款 db.transaction 仍同步(Phase 3)。
|
|
10
|
+
const candidates = await dbAll(`
|
|
7
11
|
SELECT c.*,
|
|
8
12
|
-- 审计 P0-2:优先用 snapshot 字段(claim 时锁定),fallback 到 campaign 当前值
|
|
9
13
|
COALESCE(c.snap_reach_threshold, camp.reach_threshold) as eval_threshold,
|
|
@@ -18,7 +22,7 @@ export function evaluateTrialClaims(db, generateId) {
|
|
|
18
22
|
JOIN orders o ON o.id = c.order_id
|
|
19
23
|
LEFT JOIN shareables n ON n.id = c.note_id
|
|
20
24
|
WHERE c.status = 'pending_threshold' AND c.note_id IS NOT NULL
|
|
21
|
-
`)
|
|
25
|
+
`);
|
|
22
26
|
for (const r of candidates) {
|
|
23
27
|
evaluated++;
|
|
24
28
|
try {
|
|
@@ -38,7 +42,7 @@ export function evaluateTrialClaims(db, generateId) {
|
|
|
38
42
|
continue;
|
|
39
43
|
// 30 天兜底超时
|
|
40
44
|
if (liveDays > 30) {
|
|
41
|
-
db.prepare("UPDATE product_trial_claims SET status='expired', expired_at=datetime('now'), last_eval_at=datetime('now') WHERE id=?").run(r.id);
|
|
45
|
+
db.prepare("UPDATE product_trial_claims SET status='expired', expired_at=datetime('now'), last_eval_at=datetime('now') WHERE id=? AND status='pending_threshold'").run(r.id);
|
|
42
46
|
expired++;
|
|
43
47
|
continue;
|
|
44
48
|
}
|
|
@@ -58,20 +62,27 @@ export function evaluateTrialClaims(db, generateId) {
|
|
|
58
62
|
// 达阈 → 退款。卖家钱包扣 refund_amount,买家钱包加(不变 escrow / commission)
|
|
59
63
|
const amount = Number(r.order_amount || 0);
|
|
60
64
|
if (amount <= 0) {
|
|
61
|
-
db.prepare("UPDATE product_trial_claims SET reach_score=?, metrics_json=?, last_eval_at=datetime('now') WHERE id=?").run(reachScore, metricsJson, r.id);
|
|
65
|
+
db.prepare("UPDATE product_trial_claims SET reach_score=?, metrics_json=?, last_eval_at=datetime('now') WHERE id=? AND status='pending_threshold'").run(reachScore, metricsJson, r.id);
|
|
62
66
|
continue;
|
|
63
67
|
}
|
|
64
68
|
const seller = db.prepare("SELECT balance FROM wallets WHERE user_id = ?").get(r.seller_id);
|
|
65
69
|
if (!seller || Number(seller.balance) < amount) {
|
|
66
70
|
// 卖家余额不足 — 暂不退,下次再评(卖家可能补充余额)
|
|
67
|
-
db.prepare("UPDATE product_trial_claims SET reach_score=?, metrics_json=?, last_eval_at=datetime('now') WHERE id=?").run(reachScore, metricsJson, r.id);
|
|
71
|
+
db.prepare("UPDATE product_trial_claims SET reach_score=?, metrics_json=?, last_eval_at=datetime('now') WHERE id=? AND status='pending_threshold'").run(reachScore, metricsJson, r.id);
|
|
68
72
|
continue;
|
|
69
73
|
}
|
|
70
74
|
const tx = db.transaction(() => {
|
|
71
|
-
|
|
75
|
+
// Codex #233 P1:先用 CAS 抢占 claim(pending_threshold→refunded),changes!==1 说明
|
|
76
|
+
// 并发 eval(cron + admin 手动 / 重叠调用)已退过 → 抛回滚,杜绝双退。先于任何钱包写。
|
|
77
|
+
const claimed = db.prepare(`UPDATE product_trial_claims SET status='refunded', refund_amount=?, refunded_at=datetime('now'),
|
|
78
|
+
reach_score=?, metrics_json=?, last_eval_at=datetime('now') WHERE id=? AND status='pending_threshold'`).run(amount, reachScore, metricsJson, r.id);
|
|
79
|
+
if (claimed.changes !== 1)
|
|
80
|
+
throw new Error('TRIAL_ALREADY_SETTLED');
|
|
81
|
+
// 卖家扣款带余额守卫(balance>=amount);changes!==1 → 余额在预检后已变 → 抛回滚,买家不入账
|
|
82
|
+
const debited = db.prepare("UPDATE wallets SET balance = balance - ?, updated_at=datetime('now') WHERE user_id = ? AND balance >= ?").run(amount, r.seller_id, amount);
|
|
83
|
+
if (debited.changes !== 1)
|
|
84
|
+
throw new Error('TRIAL_SELLER_INSUFFICIENT');
|
|
72
85
|
db.prepare("INSERT INTO wallets (user_id, balance) VALUES (?, ?) ON CONFLICT(user_id) DO UPDATE SET balance = balance + ?, updated_at=datetime('now')").run(r.buyer_id, amount, amount);
|
|
73
|
-
db.prepare(`UPDATE product_trial_claims SET status='refunded', refund_amount=?, refunded_at=datetime('now'),
|
|
74
|
-
reach_score=?, metrics_json=?, last_eval_at=datetime('now') WHERE id=?`).run(amount, reachScore, metricsJson, r.id);
|
|
75
86
|
try {
|
|
76
87
|
// notifications schema 没有 data 列;用 actions(JSON 数组)存可点击跳转
|
|
77
88
|
db.prepare(`INSERT INTO notifications (id, user_id, type, title, body, actions)
|
|
@@ -81,11 +92,19 @@ export function evaluateTrialClaims(db, generateId) {
|
|
|
81
92
|
}
|
|
82
93
|
catch { /* notifications 表可能用旧 schema, 忽略 */ }
|
|
83
94
|
});
|
|
84
|
-
|
|
85
|
-
|
|
95
|
+
try {
|
|
96
|
+
tx();
|
|
97
|
+
refunded++;
|
|
98
|
+
}
|
|
99
|
+
catch (e) {
|
|
100
|
+
const msg = e.message;
|
|
101
|
+
// 并发已结算 / 卖家余额已变 → 跳过(下次评估再处理),非异常,不计退款
|
|
102
|
+
if (msg !== 'TRIAL_ALREADY_SETTLED' && msg !== 'TRIAL_SELLER_INSUFFICIENT')
|
|
103
|
+
throw e;
|
|
104
|
+
}
|
|
86
105
|
}
|
|
87
106
|
else {
|
|
88
|
-
db.prepare("UPDATE product_trial_claims SET reach_score=?, metrics_json=?, last_eval_at=datetime('now') WHERE id=?").run(reachScore, metricsJson, r.id);
|
|
107
|
+
db.prepare("UPDATE product_trial_claims SET reach_score=?, metrics_json=?, last_eval_at=datetime('now') WHERE id=? AND status='pending_threshold'").run(reachScore, metricsJson, r.id);
|
|
89
108
|
}
|
|
90
109
|
}
|
|
91
110
|
catch (e) {
|
|
@@ -97,11 +116,11 @@ export function evaluateTrialClaims(db, generateId) {
|
|
|
97
116
|
export function registerTrialRoutes(app, deps) {
|
|
98
117
|
const { db, generateId, auth, clientIpHash, clientUaHash, requireProtocolAdmin } = deps;
|
|
99
118
|
// 卖家:开/更新活动
|
|
100
|
-
app.post('/api/products/:product_id/trial-campaign', (req, res) => {
|
|
119
|
+
app.post('/api/products/:product_id/trial-campaign', async (req, res) => {
|
|
101
120
|
const user = auth(req, res);
|
|
102
121
|
if (!user)
|
|
103
122
|
return;
|
|
104
|
-
const product =
|
|
123
|
+
const product = await dbOne('SELECT id, seller_id, status FROM products WHERE id = ?', [req.params.product_id]);
|
|
105
124
|
if (!product)
|
|
106
125
|
return void res.status(404).json({ error: '商品不存在' });
|
|
107
126
|
if (product.seller_id !== user.id)
|
|
@@ -121,7 +140,7 @@ export function registerTrialRoutes(app, deps) {
|
|
|
121
140
|
return void res.status(400).json({ error: 'min_days_live 需在 1-90 之间' });
|
|
122
141
|
// B3 修:1 product 1 row(UNIQUE)所以"关闭后再开"必须走 UPDATE 路径,不再 INSERT
|
|
123
142
|
// 查任意 status 的现存行;存在即 UPDATE,不存在才 INSERT
|
|
124
|
-
const existing =
|
|
143
|
+
const existing = await dbOne("SELECT id, status, quota_claimed, reach_threshold, min_chars, min_days_live FROM product_trial_campaigns WHERE product_id = ?", [product.id]);
|
|
125
144
|
if (existing) {
|
|
126
145
|
if (quota < existing.quota_claimed)
|
|
127
146
|
return void res.status(400).json({ error: `quota_total 不可低于已申请数 ${existing.quota_claimed}` });
|
|
@@ -135,35 +154,34 @@ export function registerTrialRoutes(app, deps) {
|
|
|
135
154
|
return void res.status(400).json({ error: `已有申请,min_days_live 不可上调(当前 ${existing.min_days_live})` });
|
|
136
155
|
}
|
|
137
156
|
// 重开:status='closed' → 重置为 active 且清 closed_at
|
|
138
|
-
|
|
157
|
+
await dbRun(`UPDATE product_trial_campaigns
|
|
139
158
|
SET quota_total=?, reach_threshold=?, min_chars=?, min_days_live=?,
|
|
140
159
|
status='active', closed_at=NULL
|
|
141
|
-
WHERE id
|
|
142
|
-
.run(quota, threshold, minChars, minDays, existing.id);
|
|
160
|
+
WHERE id=?`, [quota, threshold, minChars, minDays, existing.id]);
|
|
143
161
|
return void res.json({ ok: true, campaign_id: existing.id, updated: true, reopened: existing.status !== 'active' });
|
|
144
162
|
}
|
|
145
163
|
const id = generateId('ptc');
|
|
146
|
-
|
|
147
|
-
VALUES (?,?,?,?,?,?,?)
|
|
164
|
+
await dbRun(`INSERT INTO product_trial_campaigns (id, product_id, seller_id, quota_total, reach_threshold, min_chars, min_days_live)
|
|
165
|
+
VALUES (?,?,?,?,?,?,?)`, [id, product.id, user.id, quota, threshold, minChars, minDays]);
|
|
148
166
|
res.json({ ok: true, campaign_id: id, created: true });
|
|
149
167
|
});
|
|
150
168
|
// 卖家关闭活动(仍允许 pending claims 完成评估)
|
|
151
|
-
app.delete('/api/products/:product_id/trial-campaign', (req, res) => {
|
|
169
|
+
app.delete('/api/products/:product_id/trial-campaign', async (req, res) => {
|
|
152
170
|
const user = auth(req, res);
|
|
153
171
|
if (!user)
|
|
154
172
|
return;
|
|
155
|
-
const camp =
|
|
173
|
+
const camp = await dbOne("SELECT id, seller_id FROM product_trial_campaigns WHERE product_id = ? AND status = 'active'", [req.params.product_id]);
|
|
156
174
|
if (!camp)
|
|
157
175
|
return void res.status(404).json({ error: '无活跃活动' });
|
|
158
176
|
if (camp.seller_id !== user.id)
|
|
159
177
|
return void res.status(403).json({ error: '仅卖家可关闭' });
|
|
160
|
-
|
|
178
|
+
await dbRun("UPDATE product_trial_campaigns SET status='closed', closed_at=datetime('now') WHERE id=?", [camp.id]);
|
|
161
179
|
res.json({ ok: true, closed: true });
|
|
162
180
|
});
|
|
163
181
|
// 公开查询商品的活动状态(任何人)
|
|
164
|
-
app.get('/api/products/:product_id/trial-campaign', (req, res) => {
|
|
165
|
-
const camp =
|
|
166
|
-
FROM product_trial_campaigns WHERE product_id = ? AND status = 'active'
|
|
182
|
+
app.get('/api/products/:product_id/trial-campaign', async (req, res) => {
|
|
183
|
+
const camp = await dbOne(`SELECT id, quota_total, quota_claimed, reach_threshold, min_chars, min_days_live, status, created_at
|
|
184
|
+
FROM product_trial_campaigns WHERE product_id = ? AND status = 'active'`, [req.params.product_id]);
|
|
167
185
|
if (!camp)
|
|
168
186
|
return void res.json({ campaign: null });
|
|
169
187
|
res.json({ campaign: { ...camp, quota_remaining: Number(camp.quota_total) - Number(camp.quota_claimed) } });
|
|
@@ -173,12 +191,12 @@ export function registerTrialRoutes(app, deps) {
|
|
|
173
191
|
// P0: 拒绝 buyer_id === seller_id(自买自评)
|
|
174
192
|
// P0: 快照 campaign 配置到 claim 行,cron 按快照评估(防卖家中途上调阈值)
|
|
175
193
|
// P1: 新账号 < 3 天禁申请;IP/UA 与卖家 session 重叠 → 标 account_link 审计 flag
|
|
176
|
-
app.post('/api/products/:product_id/trial-claim', (req, res) => {
|
|
194
|
+
app.post('/api/products/:product_id/trial-claim', async (req, res) => {
|
|
177
195
|
const user = auth(req, res);
|
|
178
196
|
if (!user)
|
|
179
197
|
return;
|
|
180
198
|
const productId = req.params.product_id;
|
|
181
|
-
const camp =
|
|
199
|
+
const camp = await dbOne("SELECT * FROM product_trial_campaigns WHERE product_id = ? AND status = 'active'", [productId]);
|
|
182
200
|
if (!camp)
|
|
183
201
|
return void res.status(404).json({ error: '该商品当前无测评活动' });
|
|
184
202
|
if (Number(camp.quota_claimed) >= Number(camp.quota_total))
|
|
@@ -194,24 +212,24 @@ export function registerTrialRoutes(app, deps) {
|
|
|
194
212
|
return void res.status(403).json({ error: `新账号需注册满 3 天才能申请测评(你已注册 ${ageDays.toFixed(1)} 天)` });
|
|
195
213
|
}
|
|
196
214
|
// 买家必须有该商品的 confirmed/completed 订单
|
|
197
|
-
const order =
|
|
215
|
+
const order = await dbOne(`SELECT id, total_amount FROM orders WHERE product_id = ? AND buyer_id = ? AND status IN ('confirmed','completed') ORDER BY created_at DESC LIMIT 1`, [productId, user.id]);
|
|
198
216
|
if (!order)
|
|
199
217
|
return void res.status(400).json({ error: '需先完成订单 (confirmed 或 completed) 才能申请测评' });
|
|
200
|
-
const dup =
|
|
218
|
+
const dup = await dbOne("SELECT id FROM product_trial_claims WHERE buyer_id = ? AND product_id = ?", [user.id, productId]);
|
|
201
219
|
if (dup)
|
|
202
220
|
return void res.status(409).json({ error: '已申请过该商品测评', existing_id: dup.id });
|
|
203
221
|
// 审计 P1-7(2026-05-25):单 IP 1h 频次限制 — 防脚本批量小号撞名额
|
|
204
222
|
// #1016 fix: 实际列名是 claimed_at(datetime('now') default),不是 created_at
|
|
205
223
|
const buyerIp = clientIpHash(req);
|
|
206
224
|
const buyerUa = clientUaHash(req);
|
|
207
|
-
const recentIpClaims =
|
|
208
|
-
WHERE buyer_ip_hash = ? AND claimed_at > datetime('now', '-1 hour')
|
|
225
|
+
const recentIpClaims = (await dbOne(`SELECT COUNT(*) as n FROM product_trial_claims
|
|
226
|
+
WHERE buyer_ip_hash = ? AND claimed_at > datetime('now', '-1 hour')`, [buyerIp])).n;
|
|
209
227
|
if (recentIpClaims >= 3) {
|
|
210
228
|
return void res.status(429).json({ error: '当前网络申请过于频繁,请稍后再试', error_code: 'TRIAL_IP_RATE_LIMITED' });
|
|
211
229
|
}
|
|
212
230
|
// 审计 P1-2:IP/UA 重叠检测 — 标记 flag,不阻断(阻断会误伤共享网络的真买家)
|
|
213
|
-
const linkRow =
|
|
214
|
-
WHERE user_id = ? AND (ip = ? OR fingerprint_hash = ?) LIMIT 1
|
|
231
|
+
const linkRow = await dbOne(`SELECT 1 FROM user_sessions
|
|
232
|
+
WHERE user_id = ? AND (ip = ? OR fingerprint_hash = ?) LIMIT 1`, [camp.seller_id, buyerIp, buyerUa]);
|
|
215
233
|
const flags = [];
|
|
216
234
|
if (linkRow)
|
|
217
235
|
flags.push('account_link_ip_or_ua');
|
|
@@ -240,11 +258,11 @@ export function registerTrialRoutes(app, deps) {
|
|
|
240
258
|
res.json({ ok: true, claim_id: id, refund_eligible_amount: order.total_amount, audit_flags: flags });
|
|
241
259
|
});
|
|
242
260
|
// 买家关联笔记
|
|
243
|
-
app.post('/api/trial-claims/:claim_id/link-note', (req, res) => {
|
|
261
|
+
app.post('/api/trial-claims/:claim_id/link-note', async (req, res) => {
|
|
244
262
|
const user = auth(req, res);
|
|
245
263
|
if (!user)
|
|
246
264
|
return;
|
|
247
|
-
const claim =
|
|
265
|
+
const claim = await dbOne("SELECT * FROM product_trial_claims WHERE id = ?", [req.params.claim_id]);
|
|
248
266
|
if (!claim)
|
|
249
267
|
return void res.status(404).json({ error: '申请不存在' });
|
|
250
268
|
if (claim.buyer_id !== user.id)
|
|
@@ -255,8 +273,8 @@ export function registerTrialRoutes(app, deps) {
|
|
|
255
273
|
if (!note_id)
|
|
256
274
|
return void res.status(400).json({ error: '缺少 note_id' });
|
|
257
275
|
// 笔记必须存在 + 是 type=note + owner=买家 + related_product_id=该商品
|
|
258
|
-
const note =
|
|
259
|
-
FROM shareables WHERE id =
|
|
276
|
+
const note = await dbOne(`SELECT id, owner_id, type, related_product_id, native_text, photo_hashes, status, created_at
|
|
277
|
+
FROM shareables WHERE id = ?`, [note_id]);
|
|
260
278
|
if (!note)
|
|
261
279
|
return void res.status(404).json({ error: '笔记不存在' });
|
|
262
280
|
if (note.owner_id !== user.id)
|
|
@@ -276,58 +294,58 @@ export function registerTrialRoutes(app, deps) {
|
|
|
276
294
|
const photoHashes = note.photo_hashes ? JSON.parse(String(note.photo_hashes)) : [];
|
|
277
295
|
if (!Array.isArray(photoHashes) || photoHashes.length === 0)
|
|
278
296
|
return void res.status(400).json({ error: '笔记需至少 1 张图' });
|
|
279
|
-
|
|
297
|
+
await dbRun(`UPDATE product_trial_claims SET note_id=?, note_linked_at=datetime('now'), status='pending_threshold' WHERE id=?`, [note_id, claim.id]);
|
|
280
298
|
res.json({ ok: true, status: 'pending_threshold' });
|
|
281
299
|
});
|
|
282
300
|
// 买家:我的测评列表
|
|
283
|
-
app.get('/api/me/trial-claims', (req, res) => {
|
|
301
|
+
app.get('/api/me/trial-claims', async (req, res) => {
|
|
284
302
|
const user = auth(req, res);
|
|
285
303
|
if (!user)
|
|
286
304
|
return;
|
|
287
|
-
const rows =
|
|
305
|
+
const rows = await dbAll(`SELECT c.*, p.title as product_title, p.price as product_price,
|
|
288
306
|
camp.reach_threshold, camp.min_days_live
|
|
289
307
|
FROM product_trial_claims c
|
|
290
308
|
LEFT JOIN products p ON p.id = c.product_id
|
|
291
309
|
LEFT JOIN product_trial_campaigns camp ON camp.id = c.campaign_id
|
|
292
310
|
WHERE c.buyer_id = ?
|
|
293
|
-
ORDER BY c.claimed_at DESC LIMIT 100
|
|
311
|
+
ORDER BY c.claimed_at DESC LIMIT 100`, [user.id]);
|
|
294
312
|
res.json({ items: rows });
|
|
295
313
|
});
|
|
296
314
|
// 卖家:我的测评活动列表(含每个的 claims 计数)
|
|
297
|
-
app.get('/api/me/seller/trial-campaigns', (req, res) => {
|
|
315
|
+
app.get('/api/me/seller/trial-campaigns', async (req, res) => {
|
|
298
316
|
const user = auth(req, res);
|
|
299
317
|
if (!user)
|
|
300
318
|
return;
|
|
301
|
-
const rows =
|
|
319
|
+
const rows = await dbAll(`SELECT camp.*, p.title as product_title, p.price as product_price,
|
|
302
320
|
(SELECT COUNT(*) FROM product_trial_claims WHERE campaign_id = camp.id AND status='refunded') as refunded_count,
|
|
303
321
|
(SELECT COUNT(*) FROM product_trial_claims WHERE campaign_id = camp.id AND status='pending_threshold') as evaluating_count,
|
|
304
322
|
(SELECT COUNT(*) FROM product_trial_claims WHERE campaign_id = camp.id AND status='expired') as expired_count
|
|
305
323
|
FROM product_trial_campaigns camp
|
|
306
324
|
LEFT JOIN products p ON p.id = camp.product_id
|
|
307
325
|
WHERE camp.seller_id = ?
|
|
308
|
-
ORDER BY camp.created_at DESC LIMIT 100
|
|
326
|
+
ORDER BY camp.created_at DESC LIMIT 100`, [user.id]);
|
|
309
327
|
res.json({ items: rows });
|
|
310
328
|
});
|
|
311
329
|
// 卖家:查看某活动的 claims 详情
|
|
312
|
-
app.get('/api/trial-campaigns/:campaign_id/claims', (req, res) => {
|
|
330
|
+
app.get('/api/trial-campaigns/:campaign_id/claims', async (req, res) => {
|
|
313
331
|
const user = auth(req, res);
|
|
314
332
|
if (!user)
|
|
315
333
|
return;
|
|
316
|
-
const camp =
|
|
334
|
+
const camp = await dbOne("SELECT id, seller_id FROM product_trial_campaigns WHERE id = ?", [req.params.campaign_id]);
|
|
317
335
|
if (!camp)
|
|
318
336
|
return void res.status(404).json({ error: '活动不存在' });
|
|
319
337
|
if (camp.seller_id !== user.id)
|
|
320
338
|
return void res.status(403).json({ error: '仅卖家可查看' });
|
|
321
|
-
const rows =
|
|
339
|
+
const rows = await dbAll(`SELECT c.*, u.handle as buyer_handle, u.created_at as buyer_created_at FROM product_trial_claims c
|
|
322
340
|
LEFT JOIN users u ON u.id = c.buyer_id
|
|
323
|
-
WHERE c.campaign_id = ? ORDER BY c.claimed_at DESC
|
|
341
|
+
WHERE c.campaign_id = ? ORDER BY c.claimed_at DESC`, [camp.id]);
|
|
324
342
|
res.json({ items: rows });
|
|
325
343
|
});
|
|
326
344
|
// Admin 手动触发测评评估(测试 + 紧急 + 立即生效)
|
|
327
|
-
app.post('/api/admin/trial/run-eval', (req, res) => {
|
|
345
|
+
app.post('/api/admin/trial/run-eval', async (req, res) => {
|
|
328
346
|
const admin = requireProtocolAdmin(req, res);
|
|
329
347
|
if (!admin)
|
|
330
348
|
return;
|
|
331
|
-
res.json(evaluateTrialClaims(db, generateId));
|
|
349
|
+
res.json(await evaluateTrialClaims(db, generateId));
|
|
332
350
|
});
|
|
333
351
|
}
|
|
@@ -1,28 +1,30 @@
|
|
|
1
|
+
import { dbOne } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
export function registerTrustedKpiRoutes(app, deps) {
|
|
2
|
-
|
|
3
|
+
// db 已走 RFC-016 异步 seam(dbOne),不再直接用 deps.db
|
|
4
|
+
const { auth } = deps;
|
|
3
5
|
// Verifier KPI(白名单 tier / 配额 / 准确率 / 窗口奖励)
|
|
4
|
-
app.get('/api/verifier/me/kpi', (req, res) => {
|
|
6
|
+
app.get('/api/verifier/me/kpi', async (req, res) => {
|
|
5
7
|
const user = auth(req, res);
|
|
6
8
|
if (!user)
|
|
7
9
|
return;
|
|
8
10
|
const windowDays = Math.max(7, Math.min(365, Number(req.query.window) || 30));
|
|
9
|
-
const cumul =
|
|
10
|
-
const wl =
|
|
11
|
+
const cumul = await dbOne('SELECT tasks_done, tasks_correct, tasks_wrong, verify_rights FROM verifier_stats WHERE user_id = ?', [user.id]);
|
|
12
|
+
const wl = await dbOne('SELECT tier, daily_quota, tasks_today, is_system FROM verifier_whitelist WHERE user_id = ?', [user.id]);
|
|
11
13
|
// 窗口内投票数(跨多个 votes 表聚合)
|
|
12
|
-
const windowVotes =
|
|
14
|
+
const windowVotes = (await dbOne(`
|
|
13
15
|
SELECT
|
|
14
16
|
(SELECT COUNT(*) FROM claim_verification_votes WHERE verifier_id = ? AND voted_at > datetime('now', '-' || ? || ' days')) +
|
|
15
17
|
(SELECT COUNT(*) FROM product_claim_votes WHERE verifier_id = ? AND voted_at > datetime('now', '-' || ? || ' days')) +
|
|
16
18
|
(SELECT COUNT(*) FROM review_claim_votes WHERE verifier_id = ? AND voted_at > datetime('now', '-' || ? || ' days'))
|
|
17
19
|
as n
|
|
18
|
-
|
|
20
|
+
`, [user.id, windowDays, user.id, windowDays, user.id, windowDays])).n;
|
|
19
21
|
// 窗口内奖励 (reputation_events: claim_correct 等)
|
|
20
|
-
const earnedEvents =
|
|
22
|
+
const earnedEvents = (await dbOne(`
|
|
21
23
|
SELECT COALESCE(SUM(points), 0) as pts FROM reputation_events
|
|
22
24
|
WHERE user_id = ? AND event_type IN ('claim_correct', 'claim_upheld_against', 'claim_dismissed_false')
|
|
23
25
|
AND created_at > datetime('now', '-' || ? || ' days')
|
|
24
|
-
|
|
25
|
-
const wal =
|
|
26
|
+
`, [user.id, windowDays])).pts;
|
|
27
|
+
const wal = await dbOne('SELECT earned FROM wallets WHERE user_id = ?', [user.id]);
|
|
26
28
|
const accuracy = cumul && cumul.tasks_done > 0 ? cumul.tasks_correct / cumul.tasks_done : null;
|
|
27
29
|
res.json({
|
|
28
30
|
window_days: windowDays,
|
|
@@ -45,30 +47,30 @@ export function registerTrustedKpiRoutes(app, deps) {
|
|
|
45
47
|
});
|
|
46
48
|
});
|
|
47
49
|
// Arbitrator KPI(仲裁累计 + 裁决分布 + pending)
|
|
48
|
-
app.get('/api/arbitrator/me/kpi', (req, res) => {
|
|
50
|
+
app.get('/api/arbitrator/me/kpi', async (req, res) => {
|
|
49
51
|
const user = auth(req, res);
|
|
50
52
|
if (!user)
|
|
51
53
|
return;
|
|
52
54
|
const windowDays = Math.max(7, Math.min(365, Number(req.query.window) || 30));
|
|
53
55
|
const idLike = `%"${user.id}"%`;
|
|
54
|
-
const cumul =
|
|
56
|
+
const cumul = await dbOne(`
|
|
55
57
|
SELECT COUNT(*) as total,
|
|
56
58
|
SUM(CASE WHEN ruling_type = 'refund_buyer' THEN 1 ELSE 0 END) as refund_buyer_cnt,
|
|
57
59
|
SUM(CASE WHEN ruling_type = 'partial_refund' THEN 1 ELSE 0 END) as partial_cnt,
|
|
58
60
|
SUM(CASE WHEN ruling_type = 'release_seller' THEN 1 ELSE 0 END) as release_seller_cnt
|
|
59
61
|
FROM disputes WHERE assigned_arbitrators LIKE ? AND status IN ('resolved','dismissed')
|
|
60
|
-
|
|
61
|
-
const windowTotal =
|
|
62
|
+
`, [idLike]);
|
|
63
|
+
const windowTotal = (await dbOne(`
|
|
62
64
|
SELECT COUNT(*) as n FROM disputes
|
|
63
65
|
WHERE assigned_arbitrators LIKE ? AND status IN ('resolved','dismissed')
|
|
64
66
|
AND resolved_at > datetime('now', '-' || ? || ' days')
|
|
65
|
-
|
|
66
|
-
const pending =
|
|
67
|
+
`, [idLike, windowDays])).n;
|
|
68
|
+
const pending = (await dbOne(`
|
|
67
69
|
SELECT COUNT(*) as n FROM disputes
|
|
68
70
|
WHERE assigned_arbitrators LIKE ? AND status NOT IN ('resolved','dismissed')
|
|
69
|
-
|
|
70
|
-
const wl =
|
|
71
|
-
const wal =
|
|
71
|
+
`, [idLike])).n;
|
|
72
|
+
const wl = await dbOne('SELECT is_system, stake_amount FROM arbitrator_whitelist WHERE user_id = ?', [user.id]);
|
|
73
|
+
const wal = await dbOne('SELECT earned FROM wallets WHERE user_id = ?', [user.id]);
|
|
72
74
|
res.json({
|
|
73
75
|
window_days: windowDays,
|
|
74
76
|
is_external: wl ? wl.is_system === 0 : false,
|
|
@@ -1,20 +1,20 @@
|
|
|
1
|
+
import { dbOne, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
export function registerUrlClaimRoutes(app, deps) {
|
|
2
3
|
const { db, auth, safeFetch, generateId, parsePlatformUrl, getStakeDiscount, makeCommitmentHash, makeDescriptionHash, makePriceHash } = deps;
|
|
3
4
|
app.post('/api/link-challenges/:id/verify', async (req, res) => {
|
|
4
5
|
const user = auth(req, res);
|
|
5
6
|
if (!user)
|
|
6
7
|
return;
|
|
7
|
-
const challenge =
|
|
8
|
-
.get(req.params.id);
|
|
8
|
+
const challenge = await dbOne(`SELECT * FROM link_challenges WHERE id = ? AND status = 'pending'`, [req.params.id]);
|
|
9
9
|
if (!challenge)
|
|
10
10
|
return void res.json({ error: '验证码不存在或已失效' });
|
|
11
11
|
if (challenge.product_id !== undefined) {
|
|
12
|
-
const prod =
|
|
12
|
+
const prod = await dbOne('SELECT seller_id FROM products WHERE id = ?', [challenge.product_id]);
|
|
13
13
|
if (!prod || prod.seller_id !== user.id)
|
|
14
14
|
return void res.status(403).json({ error: '无权限' });
|
|
15
15
|
}
|
|
16
16
|
if (new Date(challenge.expires_at) < new Date()) {
|
|
17
|
-
|
|
17
|
+
await dbRun(`UPDATE link_challenges SET status='expired' WHERE id = ?`, [req.params.id]);
|
|
18
18
|
return void res.json({ error: '验证码已过期(48小时有效),请重新添加链接' });
|
|
19
19
|
}
|
|
20
20
|
const fullCode = `WebAZ-${challenge.code}`;
|
|
@@ -34,12 +34,27 @@ export function registerUrlClaimRoutes(app, deps) {
|
|
|
34
34
|
return void res.json({ error: '链接指向私网/localhost 或经 redirect 触达内部地址,已拦截' });
|
|
35
35
|
return void res.json({ error: `无法访问页面:${msg}` });
|
|
36
36
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
// 原子段:CAS 翻转 challenge pending→verified(防并发/重放双转移)+ 链接转移到本商品一起落。
|
|
38
|
+
let transferred = false;
|
|
39
|
+
try {
|
|
40
|
+
transferred = db.transaction(() => {
|
|
41
|
+
const cas = db.prepare(`UPDATE link_challenges SET status='verified', verified_at=datetime('now') WHERE id=? AND status='pending'`).run(req.params.id);
|
|
42
|
+
if (cas.changes === 0)
|
|
43
|
+
return false;
|
|
44
|
+
db.prepare(`UPDATE product_external_links SET product_id = ?, verify_note = '通过挑战验证,从原商品转移', verified_at = datetime('now') WHERE url = ?`)
|
|
45
|
+
.run(challenge.product_id, challenge.url);
|
|
46
|
+
return true;
|
|
47
|
+
})();
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
console.error('[url-claim challenge verify tx]', e.message);
|
|
51
|
+
return void res.status(500).json({ error: '验证失败,请重试' });
|
|
52
|
+
}
|
|
53
|
+
if (!transferred)
|
|
54
|
+
return void res.json({ error: '验证码已被处理,请刷新页面' });
|
|
40
55
|
res.json({ success: true, message: `验证成功!链接已转移到此商品。` });
|
|
41
56
|
});
|
|
42
|
-
app.post('/api/claim-url', (req, res) => {
|
|
57
|
+
app.post('/api/claim-url', async (req, res) => {
|
|
43
58
|
const user = auth(req, res);
|
|
44
59
|
if (!user)
|
|
45
60
|
return;
|
|
@@ -50,28 +65,29 @@ export function registerUrlClaimRoutes(app, deps) {
|
|
|
50
65
|
return void res.json({ error: '请填写链接、商品名、描述和价格' });
|
|
51
66
|
}
|
|
52
67
|
const claimExternalTitle = typeof external_title === 'string' && external_title.trim() ? external_title.trim() : null;
|
|
53
|
-
const otherClaim =
|
|
68
|
+
const otherClaim = await dbOne(`
|
|
54
69
|
SELECT p.id FROM product_external_links pel
|
|
55
70
|
JOIN products p ON pel.product_id = p.id
|
|
56
71
|
WHERE pel.url = ? AND pel.verified = 1 AND p.seller_id != ?
|
|
57
|
-
|
|
72
|
+
`, [url, user.id]);
|
|
58
73
|
if (!otherClaim) {
|
|
59
74
|
return void res.json({ error: '该链接当前没有其他商家认领,请直接使用导入上架功能' });
|
|
60
75
|
}
|
|
61
|
-
const existingClaim =
|
|
76
|
+
const existingClaim = await dbOne(`
|
|
62
77
|
SELECT vt.id FROM verify_tasks vt
|
|
63
78
|
JOIN products p ON vt.product_id = p.id
|
|
64
79
|
WHERE vt.url = ? AND p.seller_id = ? AND vt.status IN ('code_issued','open')
|
|
65
|
-
|
|
80
|
+
`, [url, user.id]);
|
|
66
81
|
if (existingClaim) {
|
|
67
82
|
return void res.json({ error: '您已有针对此链接的进行中认领任务,请在商品编辑页查看并确认', task_id: existingClaim.id });
|
|
68
83
|
}
|
|
69
84
|
const VERIFIERS_NEEDED = 1;
|
|
70
85
|
const REWARD_EACH = 0.1;
|
|
71
86
|
const feeLocked = VERIFIERS_NEEDED * REWARD_EACH;
|
|
72
|
-
|
|
87
|
+
// 友好预检查(读):真正的守恒门在事务内(WHERE balance >= stake+fee)。
|
|
88
|
+
const wallet = (await dbOne('SELECT balance FROM wallets WHERE user_id = ?', [user.id]));
|
|
73
89
|
const priceNum = Number(price);
|
|
74
|
-
const stakeDiscount = getStakeDiscount(db, user.id);
|
|
90
|
+
const stakeDiscount = await getStakeDiscount(db, user.id);
|
|
75
91
|
const stakeRate = Math.max(0.05, 0.15 - stakeDiscount);
|
|
76
92
|
const stakeAmount = Math.round(priceNum * stakeRate * 100) / 100;
|
|
77
93
|
if (wallet.balance < stakeAmount + feeLocked) {
|
|
@@ -81,26 +97,49 @@ export function registerUrlClaimRoutes(app, deps) {
|
|
|
81
97
|
const productId = generateId('prd');
|
|
82
98
|
const specsJson = specs ? (typeof specs === 'string' ? specs : JSON.stringify(specs)) : null;
|
|
83
99
|
const pFields = { ship_regions: '全国', handling_hours, estimated_days: null, return_days, return_condition: '', warranty_days };
|
|
84
|
-
db.prepare(`INSERT INTO products (
|
|
85
|
-
id, seller_id, title, description, price, stock, category, stake_amount,
|
|
86
|
-
specs, source_url, handling_hours, return_days, warranty_days,
|
|
87
|
-
commitment_hash, description_hash, price_hash, hashed_at, status
|
|
88
|
-
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,'warehouse')`).run(productId, user.id, title, description, priceNum, Number(stock), category, stakeAmount, specsJson, url, Number(handling_hours), Number(return_days), Number(warranty_days), makeCommitmentHash(pFields), makeDescriptionHash({ title, description, specs: specsJson }), makePriceHash(priceNum, now), now);
|
|
89
|
-
db.prepare(`UPDATE wallets SET balance = balance - ?, staked = staked + ? WHERE user_id = ?`)
|
|
90
|
-
.run(stakeAmount, stakeAmount, user.id);
|
|
91
|
-
db.prepare(`UPDATE products SET stake_locked_at = datetime('now') WHERE id = ?`).run(productId);
|
|
92
100
|
const linkId = generateId('lnk');
|
|
93
101
|
const claimUrlMeta = parsePlatformUrl(url);
|
|
94
|
-
db.prepare(`INSERT INTO product_external_links
|
|
95
|
-
(id, product_id, url, source, verified, verify_note, platform, external_id, external_title)
|
|
96
|
-
VALUES (?,?,?,'claim',0,'认领验证进行中',?,?,?)`).run(linkId, productId, url, claimUrlMeta?.platform ?? null, claimUrlMeta?.external_id ?? null, claimExternalTitle);
|
|
97
102
|
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
|
98
103
|
const code = Array.from({ length: 8 }, () => chars[Math.floor(Math.random() * chars.length)]).join('');
|
|
99
104
|
const taskId = generateId('vtk');
|
|
100
105
|
const expiresAt = new Date(Date.now() + 72 * 3600_000).toISOString();
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
106
|
+
// stake+fee 原子段:重检无进行中认领(防双任务双锁)+ 钱包一次性扣 stake+fee(守恒 guard)
|
|
107
|
+
// + 建商品(warehouse)+ 锁定时间 + INSERT 链接 + INSERT 验证任务。任一失败整段回滚。
|
|
108
|
+
// 注:原代码两次扣款(stake 一次、fee 一次)合并为一次 balance -= stake+fee, staked += stake(语义等价)。
|
|
109
|
+
try {
|
|
110
|
+
db.transaction(() => {
|
|
111
|
+
const dupTask = db.prepare(`SELECT vt.id FROM verify_tasks vt JOIN products p ON vt.product_id = p.id WHERE vt.url = ? AND p.seller_id = ? AND vt.status IN ('code_issued','open')`)
|
|
112
|
+
.get(url, user.id);
|
|
113
|
+
if (dupTask)
|
|
114
|
+
throw new Error('CLAIM_EXISTS');
|
|
115
|
+
const debit = db.prepare(`UPDATE wallets SET balance = balance - ?, staked = staked + ? WHERE user_id = ? AND balance >= ?`)
|
|
116
|
+
.run(stakeAmount + feeLocked, stakeAmount, user.id, stakeAmount + feeLocked);
|
|
117
|
+
if (debit.changes === 0)
|
|
118
|
+
throw new Error('CLAIM_INSUFFICIENT');
|
|
119
|
+
db.prepare(`INSERT INTO products (
|
|
120
|
+
id, seller_id, title, description, price, stock, category, stake_amount,
|
|
121
|
+
specs, source_url, handling_hours, return_days, warranty_days,
|
|
122
|
+
commitment_hash, description_hash, price_hash, hashed_at, status, stake_locked_at
|
|
123
|
+
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,'warehouse',datetime('now'))`)
|
|
124
|
+
.run(productId, user.id, title, description, priceNum, Number(stock), category, stakeAmount, specsJson, url, Number(handling_hours), Number(return_days), Number(warranty_days), makeCommitmentHash(pFields), makeDescriptionHash({ title, description, specs: specsJson }), makePriceHash(priceNum, now), now);
|
|
125
|
+
db.prepare(`INSERT INTO product_external_links
|
|
126
|
+
(id, product_id, url, source, verified, verify_note, platform, external_id, external_title)
|
|
127
|
+
VALUES (?,?,?,'claim',0,'认领验证进行中',?,?,?)`)
|
|
128
|
+
.run(linkId, productId, url, claimUrlMeta?.platform ?? null, claimUrlMeta?.external_id ?? null, claimExternalTitle);
|
|
129
|
+
db.prepare(`INSERT INTO verify_tasks (id, type, product_id, url, code, verifiers_needed, reward_per_verifier, fee_locked, status, expires_at)
|
|
130
|
+
VALUES (?,?,?,?,?,?,?,?,'code_issued',?)`)
|
|
131
|
+
.run(taskId, 'code_check', productId, url, code, VERIFIERS_NEEDED, REWARD_EACH, feeLocked, expiresAt);
|
|
132
|
+
})();
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
const msg = e.message;
|
|
136
|
+
if (msg === 'CLAIM_EXISTS')
|
|
137
|
+
return void res.json({ error: '您已有针对此链接的进行中认领任务,请在商品编辑页查看并确认' });
|
|
138
|
+
if (msg === 'CLAIM_INSUFFICIENT')
|
|
139
|
+
return void res.json({ error: `余额不足:需要 ${stakeAmount} WAZ 质押 + ${feeLocked} WAZ 验证费` });
|
|
140
|
+
console.error('[url-claim claim-url tx]', msg);
|
|
141
|
+
return void res.status(500).json({ error: '认领失败,请重试' });
|
|
142
|
+
}
|
|
104
143
|
res.json({
|
|
105
144
|
success: true,
|
|
106
145
|
product_id: productId,
|