@seasonkoh/webaz 0.1.23 → 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.
Files changed (187) hide show
  1. package/README.md +2 -0
  2. package/dist/layer0-foundation/L0-1-database/db-backends/pg-backend.js +51 -0
  3. package/dist/layer0-foundation/L0-1-database/db-backends/sql-dialect-datetime.js +437 -0
  4. package/dist/layer0-foundation/L0-1-database/db-backends/sql-placeholders.js +98 -0
  5. package/dist/layer0-foundation/L0-1-database/db.js +65 -0
  6. package/dist/layer0-foundation/L0-2-state-machine/order-chain.js +13 -11
  7. package/dist/layer0-foundation/L0-2-state-machine/transitions.js +1 -1
  8. package/dist/layer0-foundation/L0-5-manifest/manifest.js +13 -11
  9. package/dist/layer1-agent/L1-1-mcp-server/server.js +198 -83
  10. package/dist/layer1-agent/L1-2-external-anchor/anchor-engine.js +14 -12
  11. package/dist/layer2-business/L2-6-notifications/notification-engine.js +8 -5
  12. package/dist/layer2-business/L2-7-snf/snf-engine.js +16 -14
  13. package/dist/layer2-business/L2-8-feedback/build-feedback-engine.js +18 -10
  14. package/dist/layer2-business/L2-9-contribution/build-reputation-engine.js +37 -23
  15. package/dist/layer2-business/L2-9-contribution/build-task-agent-metadata-store.js +173 -0
  16. package/dist/layer2-business/L2-9-contribution/build-task-participation.js +47 -0
  17. package/dist/layer2-business/L2-9-contribution/build-task-read.js +222 -0
  18. package/dist/layer2-business/L2-9-contribution/build-tasks-engine.js +10 -2
  19. package/dist/layer2-business/L2-9-contribution/canonical-contribution-target.js +16 -0
  20. package/dist/layer2-business/L2-9-contribution/contribution-display-envelope.js +40 -0
  21. package/dist/layer2-business/L2-9-contribution/contribution-score-contract.js +36 -0
  22. package/dist/layer2-business/L2-9-contribution/contribution-score-evidence.js +61 -0
  23. package/dist/layer2-business/L2-9-contribution/github-credential/canonical.js +60 -0
  24. package/dist/layer2-business/L2-9-contribution/github-credential/github-credential.schema.js +140 -0
  25. package/dist/layer2-business/L2-9-contribution/github-credential/github-fetch-adapter.js +437 -0
  26. package/dist/layer2-business/L2-9-contribution/github-credential/self-consistency.js +38 -0
  27. package/dist/layer2-business/L2-9-contribution/github-credential/verifier.js +231 -0
  28. package/dist/layer2-business/L2-9-contribution/github-credential-ingestion-engine.js +145 -0
  29. package/dist/layer2-business/L2-9-contribution/github-credential-store.js +115 -0
  30. package/dist/layer2-business/L2-9-contribution/identity-binding-engine.js +134 -0
  31. package/dist/layer2-business/L2-9-contribution/identity-binding-store.js +101 -0
  32. package/dist/layer2-business/L2-9-contribution/identity-claim-challenge-engine.js +126 -0
  33. package/dist/layer2-business/L2-9-contribution/identity-claim-challenge-store.js +30 -0
  34. package/dist/layer2-business/L2-9-contribution/identity-claim-engine.js +109 -0
  35. package/dist/layer2-business/L2-9-contribution/identity-claim-fact-precondition.js +22 -0
  36. package/dist/layer2-business/L2-9-contribution/identity-claim-proof-verifier.js +97 -0
  37. package/dist/layer2-business/L2-9-contribution/identity-claim-read.js +59 -0
  38. package/dist/layer2-business/L2-9-contribution/task-proposal-store.js +129 -0
  39. package/dist/layer2-business/L2-notes/note-photo-storage.js +4 -2
  40. package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +17 -15
  41. package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +11 -8
  42. package/dist/layer4-economics/L4-3-reputation/reputation-engine.js +9 -8
  43. package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +11 -8
  44. package/dist/layer4-economics/L4-4-skill-market/skill-listing-engine.js +22 -16
  45. package/dist/pwa/acp-feed.js +13 -1
  46. package/dist/pwa/contract-fingerprint.js +2 -0
  47. package/dist/pwa/endpoint-actions.js +5 -1
  48. package/dist/pwa/goal-index.js +8 -8
  49. package/dist/pwa/human-presence.js +62 -0
  50. package/dist/pwa/public/app.js +575 -68
  51. package/dist/pwa/public/i18n.js +29 -20
  52. package/dist/pwa/public/index.html +1 -0
  53. package/dist/pwa/public/openapi.json +2 -2
  54. package/dist/pwa/rate-limit.js +22 -0
  55. package/dist/pwa/routes/account-deletion.js +15 -13
  56. package/dist/pwa/routes/addresses.js +10 -9
  57. package/dist/pwa/routes/admin-admins.js +13 -14
  58. package/dist/pwa/routes/admin-analytics.js +109 -69
  59. package/dist/pwa/routes/admin-catalog.js +13 -11
  60. package/dist/pwa/routes/admin-editor-picks.js +15 -10
  61. package/dist/pwa/routes/admin-events.js +5 -3
  62. package/dist/pwa/routes/admin-health.js +2 -1
  63. package/dist/pwa/routes/admin-moderation.js +26 -29
  64. package/dist/pwa/routes/admin-ops.js +22 -21
  65. package/dist/pwa/routes/admin-protocol-params.js +16 -19
  66. package/dist/pwa/routes/admin-reports.js +23 -21
  67. package/dist/pwa/routes/admin-tokenomics.js +26 -25
  68. package/dist/pwa/routes/admin-users-lifecycle.js +37 -40
  69. package/dist/pwa/routes/admin-users-query.js +54 -53
  70. package/dist/pwa/routes/admin-verifier-flow.js +82 -41
  71. package/dist/pwa/routes/admin-verifier-whitelist.js +55 -27
  72. package/dist/pwa/routes/admin-wallet-ops.js +7 -5
  73. package/dist/pwa/routes/agent-buy.js +46 -22
  74. package/dist/pwa/routes/agent-governance.js +52 -56
  75. package/dist/pwa/routes/ai.js +7 -5
  76. package/dist/pwa/routes/analytics.js +43 -41
  77. package/dist/pwa/routes/anchors.js +19 -20
  78. package/dist/pwa/routes/announcements.js +13 -13
  79. package/dist/pwa/routes/arbitrator.js +97 -31
  80. package/dist/pwa/routes/auction.js +153 -114
  81. package/dist/pwa/routes/auth-login.js +6 -4
  82. package/dist/pwa/routes/auth-read.js +11 -9
  83. package/dist/pwa/routes/auth-register.js +35 -20
  84. package/dist/pwa/routes/auth-sessions.js +12 -11
  85. package/dist/pwa/routes/blocklist.js +16 -15
  86. package/dist/pwa/routes/build-feedback.js +10 -9
  87. package/dist/pwa/routes/build-reputation.js +6 -2
  88. package/dist/pwa/routes/build-tasks.js +45 -13
  89. package/dist/pwa/routes/buyer-feeds.js +27 -25
  90. package/dist/pwa/routes/cart.js +16 -15
  91. package/dist/pwa/routes/charity.js +212 -150
  92. package/dist/pwa/routes/chat.js +42 -43
  93. package/dist/pwa/routes/checkin-tasks.js +10 -9
  94. package/dist/pwa/routes/checkout-helpers.js +12 -10
  95. package/dist/pwa/routes/claim-initiators.js +34 -14
  96. package/dist/pwa/routes/claim-verify.js +86 -53
  97. package/dist/pwa/routes/claim-voting.js +43 -18
  98. package/dist/pwa/routes/contribution-identity.js +147 -0
  99. package/dist/pwa/routes/contribution-score.js +19 -0
  100. package/dist/pwa/routes/coupons.js +19 -16
  101. package/dist/pwa/routes/dashboards.js +18 -16
  102. package/dist/pwa/routes/dispute-cases.js +25 -24
  103. package/dist/pwa/routes/disputes-read.js +45 -51
  104. package/dist/pwa/routes/disputes-write.js +124 -61
  105. package/dist/pwa/routes/evidence.js +9 -9
  106. package/dist/pwa/routes/external-anchors.js +13 -12
  107. package/dist/pwa/routes/feedback.js +29 -33
  108. package/dist/pwa/routes/flash-sales.js +18 -16
  109. package/dist/pwa/routes/follows.js +25 -24
  110. package/dist/pwa/routes/governance-auto-deactivate.js +21 -9
  111. package/dist/pwa/routes/governance-onboarding.js +70 -59
  112. package/dist/pwa/routes/group-buys.js +22 -22
  113. package/dist/pwa/routes/growth.js +33 -30
  114. package/dist/pwa/routes/import-product.js +12 -10
  115. package/dist/pwa/routes/kyc.js +9 -8
  116. package/dist/pwa/routes/leaderboard.js +20 -18
  117. package/dist/pwa/routes/listings.js +23 -22
  118. package/dist/pwa/routes/logistics.js +10 -8
  119. package/dist/pwa/routes/manifests.js +27 -27
  120. package/dist/pwa/routes/me-data.js +23 -21
  121. package/dist/pwa/routes/notifications.js +7 -6
  122. package/dist/pwa/routes/offers.js +30 -12
  123. package/dist/pwa/routes/orders-action.js +33 -17
  124. package/dist/pwa/routes/orders-create.js +75 -20
  125. package/dist/pwa/routes/orders-read.js +21 -20
  126. package/dist/pwa/routes/p2p-products.js +30 -18
  127. package/dist/pwa/routes/payments-governance.js +61 -56
  128. package/dist/pwa/routes/peers.js +9 -8
  129. package/dist/pwa/routes/pin-receipts.js +13 -13
  130. package/dist/pwa/routes/products-aliases.js +12 -10
  131. package/dist/pwa/routes/products-claims.js +36 -17
  132. package/dist/pwa/routes/products-create.js +53 -38
  133. package/dist/pwa/routes/products-crud.js +17 -16
  134. package/dist/pwa/routes/products-links.js +49 -26
  135. package/dist/pwa/routes/products-list.js +6 -4
  136. package/dist/pwa/routes/products-meta.js +40 -39
  137. package/dist/pwa/routes/products-update.js +19 -5
  138. package/dist/pwa/routes/profile-credentials.js +14 -16
  139. package/dist/pwa/routes/profile-identity.js +14 -13
  140. package/dist/pwa/routes/profile-location.js +7 -6
  141. package/dist/pwa/routes/profile-placement.js +19 -17
  142. package/dist/pwa/routes/profile-prefs.js +11 -11
  143. package/dist/pwa/routes/promoter.js +55 -49
  144. package/dist/pwa/routes/public-build-tasks.js +19 -0
  145. package/dist/pwa/routes/public-utils.js +108 -46
  146. package/dist/pwa/routes/push.js +16 -15
  147. package/dist/pwa/routes/ratings.js +30 -30
  148. package/dist/pwa/routes/recover-key.js +13 -12
  149. package/dist/pwa/routes/referral.js +37 -32
  150. package/dist/pwa/routes/reputation.js +3 -2
  151. package/dist/pwa/routes/returns.js +76 -73
  152. package/dist/pwa/routes/reviews.js +41 -18
  153. package/dist/pwa/routes/rewards-apply.js +16 -15
  154. package/dist/pwa/routes/rewards-auto-downgrade.js +9 -7
  155. package/dist/pwa/routes/rewards-escrow-expire.js +7 -5
  156. package/dist/pwa/routes/rfqs.js +163 -85
  157. package/dist/pwa/routes/search.js +16 -14
  158. package/dist/pwa/routes/secondhand.js +25 -22
  159. package/dist/pwa/routes/seller-quota.js +24 -26
  160. package/dist/pwa/routes/share-redirects.js +59 -55
  161. package/dist/pwa/routes/shareables-interactions.js +34 -35
  162. package/dist/pwa/routes/shareables.js +55 -51
  163. package/dist/pwa/routes/shop-referral.js +57 -0
  164. package/dist/pwa/routes/shops.js +20 -18
  165. package/dist/pwa/routes/signaling.js +10 -9
  166. package/dist/pwa/routes/skill-market.js +16 -16
  167. package/dist/pwa/routes/skills.js +15 -14
  168. package/dist/pwa/routes/snf.js +14 -13
  169. package/dist/pwa/routes/tags.js +10 -9
  170. package/dist/pwa/routes/task-proposals.js +45 -0
  171. package/dist/pwa/routes/trial.js +69 -51
  172. package/dist/pwa/routes/trusted-kpi.js +20 -18
  173. package/dist/pwa/routes/url-claim.js +67 -28
  174. package/dist/pwa/routes/users-public.js +62 -60
  175. package/dist/pwa/routes/variants.js +12 -13
  176. package/dist/pwa/routes/verifier-user.js +61 -21
  177. package/dist/pwa/routes/verify-tasks.js +49 -25
  178. package/dist/pwa/routes/waitlist.js +16 -15
  179. package/dist/pwa/routes/wallet-read.js +74 -36
  180. package/dist/pwa/routes/wallet-write.js +12 -9
  181. package/dist/pwa/routes/webauthn.js +25 -26
  182. package/dist/pwa/routes/webhooks.js +26 -26
  183. package/dist/pwa/routes/welcome.js +45 -50
  184. package/dist/pwa/routes/wishlist-qa.js +29 -32
  185. package/dist/pwa/server.js +237 -81
  186. package/dist/version.js +1 -1
  187. package/package.json +47 -2
@@ -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
- const candidates = db.prepare(`
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
- `).all();
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
- db.prepare("UPDATE wallets SET balance = balance - ?, updated_at=datetime('now') WHERE user_id = ?").run(amount, r.seller_id);
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
- tx();
85
- refunded++;
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 = db.prepare('SELECT id, seller_id, status FROM products WHERE id = ?').get(req.params.product_id);
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 = db.prepare("SELECT id, status, quota_claimed, reach_threshold, min_chars, min_days_live FROM product_trial_campaigns WHERE product_id = ?").get(product.id);
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
- db.prepare(`UPDATE product_trial_campaigns
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
- db.prepare(`INSERT INTO product_trial_campaigns (id, product_id, seller_id, quota_total, reach_threshold, min_chars, min_days_live)
147
- VALUES (?,?,?,?,?,?,?)`).run(id, product.id, user.id, quota, threshold, minChars, minDays);
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 = db.prepare("SELECT id, seller_id FROM product_trial_campaigns WHERE product_id = ? AND status = 'active'").get(req.params.product_id);
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
- db.prepare("UPDATE product_trial_campaigns SET status='closed', closed_at=datetime('now') WHERE id=?").run(camp.id);
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 = db.prepare(`SELECT id, quota_total, quota_claimed, reach_threshold, min_chars, min_days_live, status, created_at
166
- FROM product_trial_campaigns WHERE product_id = ? AND status = 'active'`).get(req.params.product_id);
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 = db.prepare("SELECT * FROM product_trial_campaigns WHERE product_id = ? AND status = 'active'").get(productId);
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 = db.prepare(`SELECT id, total_amount FROM orders WHERE product_id = ? AND buyer_id = ? AND status IN ('confirmed','completed') ORDER BY created_at DESC LIMIT 1`).get(productId, user.id);
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 = db.prepare("SELECT id FROM product_trial_claims WHERE buyer_id = ? AND product_id = ?").get(user.id, productId);
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 = db.prepare(`SELECT COUNT(*) as n FROM product_trial_claims
208
- WHERE buyer_ip_hash = ? AND claimed_at > datetime('now', '-1 hour')`).get(buyerIp).n;
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 = db.prepare(`SELECT 1 FROM user_sessions
214
- WHERE user_id = ? AND (ip = ? OR fingerprint_hash = ?) LIMIT 1`).get(camp.seller_id, buyerIp, buyerUa);
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 = db.prepare("SELECT * FROM product_trial_claims WHERE id = ?").get(req.params.claim_id);
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 = db.prepare(`SELECT id, owner_id, type, related_product_id, native_text, photo_hashes, status, created_at
259
- FROM shareables WHERE id = ?`).get(note_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
- db.prepare(`UPDATE product_trial_claims SET note_id=?, note_linked_at=datetime('now'), status='pending_threshold' WHERE id=?`).run(note_id, claim.id);
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 = db.prepare(`SELECT c.*, p.title as product_title, p.price as product_price,
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`).all(user.id);
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 = db.prepare(`SELECT camp.*, p.title as product_title, p.price as product_price,
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`).all(user.id);
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 = db.prepare("SELECT id, seller_id FROM product_trial_campaigns WHERE id = ?").get(req.params.campaign_id);
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 = db.prepare(`SELECT c.*, u.handle as buyer_handle, u.created_at as buyer_created_at FROM product_trial_claims c
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`).all(camp.id);
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
- const { db, auth } = deps;
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 = db.prepare('SELECT tasks_done, tasks_correct, tasks_wrong, verify_rights FROM verifier_stats WHERE user_id = ?').get(user.id);
10
- const wl = db.prepare('SELECT tier, daily_quota, tasks_today, is_system FROM verifier_whitelist WHERE user_id = ?').get(user.id);
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 = db.prepare(`
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
- `).get(user.id, windowDays, user.id, windowDays, user.id, windowDays).n;
20
+ `, [user.id, windowDays, user.id, windowDays, user.id, windowDays])).n;
19
21
  // 窗口内奖励 (reputation_events: claim_correct 等)
20
- const earnedEvents = db.prepare(`
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
- `).get(user.id, windowDays).pts;
25
- const wal = db.prepare('SELECT earned FROM wallets WHERE user_id = ?').get(user.id);
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 = db.prepare(`
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
- `).get(idLike);
61
- const windowTotal = db.prepare(`
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
- `).get(idLike, windowDays).n;
66
- const pending = db.prepare(`
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
- `).get(idLike).n;
70
- const wl = db.prepare('SELECT is_system, stake_amount FROM arbitrator_whitelist WHERE user_id = ?').get(user.id);
71
- const wal = db.prepare('SELECT earned FROM wallets WHERE user_id = ?').get(user.id);
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 = db.prepare(`SELECT * FROM link_challenges WHERE id = ? AND status = 'pending'`)
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 = db.prepare('SELECT seller_id FROM products WHERE id = ?').get(challenge.product_id);
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
- db.prepare(`UPDATE link_challenges SET status='expired' WHERE id = ?`).run(req.params.id);
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
- db.prepare(`UPDATE product_external_links SET product_id = ?, verify_note = '通过挑战验证,从原商品转移', verified_at = datetime('now') WHERE url = ?`)
38
- .run(challenge.product_id, challenge.url);
39
- db.prepare(`UPDATE link_challenges SET status='verified', verified_at=datetime('now') WHERE id=?`).run(req.params.id);
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 = db.prepare(`
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
- `).get(url, user.id);
72
+ `, [url, user.id]);
58
73
  if (!otherClaim) {
59
74
  return void res.json({ error: '该链接当前没有其他商家认领,请直接使用导入上架功能' });
60
75
  }
61
- const existingClaim = db.prepare(`
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
- `).get(url, user.id);
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
- const wallet = db.prepare('SELECT balance FROM wallets WHERE user_id = ?').get(user.id);
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
- db.prepare(`INSERT INTO verify_tasks (id, type, product_id, url, code, verifiers_needed, reward_per_verifier, fee_locked, status, expires_at)
102
- VALUES (?,?,?,?,?,?,?,?,'code_issued',?)`).run(taskId, 'code_check', productId, url, code, VERIFIERS_NEEDED, REWARD_EACH, feeLocked, expiresAt);
103
- db.prepare(`UPDATE wallets SET balance = balance - ? WHERE user_id = ?`).run(feeLocked, user.id);
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,