@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,4 +1,7 @@
1
1
  import { recordRepEvent } from '../../layer4-economics/L4-3-reputation/reputation-engine.js';
2
+ // RFC-016 Phase 1 — 端点校验读/列表读 + 状态翻转/消息/通知单写 → async seam;
3
+ // executeReturnRefund 退款 db.transaction(钱+库存)与 escalate 建争议 tx 保持同步(Phase 3 迁 pg)。
4
+ import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js';
2
5
  const VALID_RETURN_REASONS = new Set(['quality', 'wrong_item', 'damaged', 'no_longer_needed', 'other']);
3
6
  const RETURN_REASON_DEFAULT_LABEL = {
4
7
  quality: '质量问题',
@@ -10,15 +13,24 @@ const RETURN_REASON_DEFAULT_LABEL = {
10
13
  export function registerReturnsRoutes(app, deps) {
11
14
  const { db, generateId, auth, isTrustedRole, errorRes, broadcastSystemEvent, detectFraud } = deps;
12
15
  // L3 Phase 2 抽出:accept(无 pickup) 和 received(有 pickup) 共享退款 + 库存 + 通知
13
- function executeReturnRefund(rr, response) {
16
+ // fromStatus = 调用方允许的源状态(accept-no-pickup='pending' / received='picked_up')
17
+ // Codex #235 P1:两个端点都 await 预读 rr.status 后才进入这个同步 tx,await 间隔内
18
+ // 并发请求可都看到 pending/picked_up → 双双退款。故 tx 内先用 fromStatus CAS 抢占,
19
+ // 先于任何钱/库存写;changes!==1 即并发已结算 → 抛回滚。
20
+ function executeReturnRefund(rr, response, fromStatus) {
14
21
  db.transaction(() => {
15
22
  const refundAmt = Number(rr.refund_amount);
16
- const sellerWal = db.prepare('SELECT balance FROM wallets WHERE user_id = ?').get(rr.seller_id);
17
- if (!sellerWal || Number(sellerWal.balance) < refundAmt) {
23
+ // 1. CAS 抢占 return (fromStatus→refunded),先于任何写
24
+ const claimed = db.prepare(`UPDATE return_requests SET status = 'refunded', seller_response = COALESCE(?, seller_response), resolved_at = datetime('now') WHERE id = ? AND status = ?`)
25
+ .run(response, rr.id, fromStatus);
26
+ if (claimed.changes !== 1)
27
+ throw new Error('RETURN_ALREADY_SETTLED');
28
+ // 2. 卖家扣款带余额守卫;changes!==1 = 余额不足 → 抛回滚,买家不入账
29
+ const debited = db.prepare('UPDATE wallets SET balance = balance - ? WHERE user_id = ? AND balance >= ?').run(refundAmt, rr.seller_id, refundAmt);
30
+ if (debited.changes !== 1)
18
31
  throw new Error('INSUFFICIENT_SELLER_BALANCE');
19
- }
20
- db.prepare('UPDATE wallets SET balance = balance - ? WHERE user_id = ?').run(refundAmt, rr.seller_id);
21
32
  db.prepare('UPDATE wallets SET balance = balance + ? WHERE user_id = ?').run(refundAmt, rr.buyer_id);
33
+ // 3. 恢复库存(CAS + 扣款成功后)
22
34
  const ord = db.prepare('SELECT quantity, source, variant_id FROM orders WHERE id = ?').get(rr.order_id);
23
35
  if (ord && ord.source !== 'secondhand') {
24
36
  const qty = Math.max(1, Number(ord.quantity) || 1);
@@ -27,8 +39,6 @@ export function registerReturnsRoutes(app, deps) {
27
39
  db.prepare('UPDATE product_variants SET stock = stock + ?, updated_at = datetime(\'now\') WHERE id = ?').run(qty, ord.variant_id);
28
40
  }
29
41
  }
30
- db.prepare(`UPDATE return_requests SET status = 'refunded', seller_response = COALESCE(?, seller_response), resolved_at = datetime('now') WHERE id = ?`)
31
- .run(response, rr.id);
32
42
  try {
33
43
  db.prepare(`INSERT INTO return_messages (id, return_id, sender_id, sender_role, body) VALUES (?,?,?,?,?)`)
34
44
  .run(generateId('rmsg'), rr.id, rr.seller_id, 'seller', `[✓ 已退款] ${response || ''}`);
@@ -51,18 +61,18 @@ export function registerReturnsRoutes(app, deps) {
51
61
  }
52
62
  }
53
63
  // buyer 发起退货
54
- app.post('/api/orders/:order_id/return-request', (req, res) => {
64
+ app.post('/api/orders/:order_id/return-request', async (req, res) => {
55
65
  const user = auth(req, res);
56
66
  if (!user)
57
67
  return;
58
68
  if (isTrustedRole(user))
59
69
  return void errorRes(res, 403, 'TRUSTED_ROLE_NO_TRADE', '受信角色无购物功能');
60
- const order = db.prepare(`
70
+ const order = await dbOne(`
61
71
  SELECT o.id, o.buyer_id, o.seller_id, o.product_id, o.status, o.total_amount, o.created_at, o.updated_at,
62
72
  p.return_days, p.title as product_title
63
73
  FROM orders o JOIN products p ON p.id = o.product_id
64
74
  WHERE o.id = ?
65
- `).get(req.params.order_id);
75
+ `, [req.params.order_id]);
66
76
  if (!order)
67
77
  return void res.status(404).json({ error: '订单不存在' });
68
78
  if (order.buyer_id !== user.id)
@@ -79,9 +89,9 @@ export function registerReturnsRoutes(app, deps) {
79
89
  if (Date.now() > deadlineMs) {
80
90
  return void res.status(400).json({ error: `已超过 ${returnDays} 天退货窗口` });
81
91
  }
82
- const existing = db.prepare(`
92
+ const existing = await dbOne(`
83
93
  SELECT id, status FROM return_requests WHERE order_id = ? AND status IN ('pending', 'accepted') LIMIT 1
84
- `).get(order.id);
94
+ `, [order.id]);
85
95
  if (existing)
86
96
  return void res.status(400).json({ error: `已存在退货请求 (${existing.status})` });
87
97
  const reason = String(req.body?.reason || '');
@@ -101,15 +111,14 @@ export function registerReturnsRoutes(app, deps) {
101
111
  return void res.status(400).json({ error: '请求上门取件时必须提供取件地址(≥ 4 字)' });
102
112
  }
103
113
  const reqId = generateId('ret');
104
- db.prepare(`
114
+ await dbRun(`
105
115
  INSERT INTO return_requests (id, order_id, buyer_id, seller_id, product_id, reason, reason_text, refund_amount, status, pickup_requested, pickup_address)
106
116
  VALUES (?,?,?,?,?,?,?,?,'pending',?,?)
107
- `).run(reqId, order.id, order.buyer_id, order.seller_id, order.product_id, reason, reasonText, refundAmount, pickupRequested, pickupAddress);
117
+ `, [reqId, order.id, order.buyer_id, order.seller_id, order.product_id, reason, reasonText, refundAmount, pickupRequested, pickupAddress]);
108
118
  try {
109
119
  const actions = JSON.stringify([{ kind: 'navigate', label: '处理退货', href: `#order/${order.id}`, style: 'primary' }]);
110
120
  const pickupNote = pickupRequested ? '(含上门取件请求)' : '';
111
- db.prepare(`INSERT INTO notifications (id, user_id, type, title, body, order_id, actions) VALUES (?,?,?,?,?,?,?)`)
112
- .run(generateId('ntf'), order.seller_id, 'return_request', '⚠ 收到退货请求' + pickupNote, `订单 ${order.product_title} 申请退货 — 原因:${reason}${pickupRequested ? '\n📍 上门取件:' + pickupAddress : ''}`, order.id, actions);
121
+ await dbRun(`INSERT INTO notifications (id, user_id, type, title, body, order_id, actions) VALUES (?,?,?,?,?,?,?)`, [generateId('ntf'), order.seller_id, 'return_request', '⚠ 收到退货请求' + pickupNote, `订单 ${order.product_title} 申请退货 — 原因:${reason}${pickupRequested ? '\n📍 上门取件:' + pickupAddress : ''}`, order.id, actions]);
113
122
  }
114
123
  catch (e) {
115
124
  console.error('[return notify]', e);
@@ -121,26 +130,26 @@ export function registerReturnsRoutes(app, deps) {
121
130
  res.json({ success: true, id: reqId, pickup_requested: !!pickupRequested });
122
131
  });
123
132
  // P1-5: 订单级直查
124
- app.get('/api/orders/:order_id/return-request', (req, res) => {
133
+ app.get('/api/orders/:order_id/return-request', async (req, res) => {
125
134
  const user = auth(req, res);
126
135
  if (!user)
127
136
  return;
128
- const order = db.prepare('SELECT buyer_id, seller_id FROM orders WHERE id = ?').get(req.params.order_id);
137
+ const order = await dbOne('SELECT buyer_id, seller_id FROM orders WHERE id = ?', [req.params.order_id]);
129
138
  if (!order)
130
139
  return void res.status(404).json({ error: '订单不存在' });
131
140
  if (order.buyer_id !== user.id && order.seller_id !== user.id) {
132
141
  return void res.status(403).json({ error: '无权查看' });
133
142
  }
134
- const row = db.prepare(`
143
+ const row = await dbOne(`
135
144
  SELECT id, order_id, product_id, reason, reason_text, refund_amount,
136
145
  status, seller_response, escalated_dispute_id, created_at, resolved_at
137
146
  FROM return_requests
138
147
  WHERE order_id = ?
139
148
  ORDER BY created_at DESC LIMIT 1
140
- `).get(req.params.order_id);
149
+ `, [req.params.order_id]);
141
150
  res.json({ item: row || null });
142
151
  });
143
- app.get('/api/return-requests', (req, res) => {
152
+ app.get('/api/return-requests', async (req, res) => {
144
153
  const user = auth(req, res);
145
154
  if (!user)
146
155
  return;
@@ -153,7 +162,7 @@ export function registerReturnsRoutes(app, deps) {
153
162
  where.push('r.status = ?');
154
163
  params.push(status);
155
164
  }
156
- const rows = db.prepare(`
165
+ const rows = await dbAll(`
157
166
  SELECT r.id, r.order_id, r.product_id, r.reason, r.reason_text, r.refund_amount,
158
167
  r.status, r.seller_response, r.escalated_dispute_id, r.created_at, r.resolved_at,
159
168
  p.title as product_title, p.category,
@@ -167,14 +176,14 @@ export function registerReturnsRoutes(app, deps) {
167
176
  JOIN users us ON us.id = r.seller_id
168
177
  WHERE ${where.join(' AND ')}
169
178
  ORDER BY r.created_at DESC LIMIT 200
170
- `).all(...params);
179
+ `, params);
171
180
  res.json({ items: rows });
172
181
  });
173
- app.post('/api/return-requests/:id/decide', (req, res) => {
182
+ app.post('/api/return-requests/:id/decide', async (req, res) => {
174
183
  const user = auth(req, res);
175
184
  if (!user)
176
185
  return;
177
- const rr = db.prepare(`SELECT * FROM return_requests WHERE id = ?`).get(req.params.id);
186
+ const rr = await dbOne(`SELECT * FROM return_requests WHERE id = ?`, [req.params.id]);
178
187
  if (!rr)
179
188
  return void res.status(404).json({ error: '退货请求不存在' });
180
189
  if (rr.seller_id !== user.id)
@@ -189,65 +198,62 @@ export function registerReturnsRoutes(app, deps) {
189
198
  return void res.status(400).json({ error: '拒绝时必须填写说明' });
190
199
  if (decision === 'accept') {
191
200
  if (Number(rr.pickup_requested) === 1) {
192
- db.prepare(`UPDATE return_requests SET status = 'accepted_pickup_pending', seller_response = ? WHERE id = ?`)
193
- .run(response, rr.id);
201
+ await dbRun(`UPDATE return_requests SET status = 'accepted_pickup_pending', seller_response = ? WHERE id = ?`, [response, rr.id]);
194
202
  try {
195
- db.prepare(`INSERT INTO return_messages (id, return_id, sender_id, sender_role, body) VALUES (?,?,?,?,?)`)
196
- .run(generateId('rmsg'), rr.id, rr.seller_id, 'seller', `[✓ 同意 · 等待上门取件] ${response || ''}`);
203
+ await dbRun(`INSERT INTO return_messages (id, return_id, sender_id, sender_role, body) VALUES (?,?,?,?,?)`, [generateId('rmsg'), rr.id, rr.seller_id, 'seller', `[✓ 同意 · 等待上门取件] ${response || ''}`]);
197
204
  }
198
205
  catch { }
199
206
  try {
200
- db.prepare(`INSERT INTO notifications (id, user_id, title, body, order_id) VALUES (?,?,?,?,?)`)
201
- .run(generateId('ntf'), rr.buyer_id, '✓ 退货已接受 · 等待上门取件', `卖家将安排物流到 ${rr.pickup_address || '指定地址'} 上门取件`, rr.order_id);
207
+ await dbRun(`INSERT INTO notifications (id, user_id, title, body, order_id) VALUES (?,?,?,?,?)`, [generateId('ntf'), rr.buyer_id, '✓ 退货已接受 · 等待上门取件', `卖家将安排物流到 ${rr.pickup_address || '指定地址'} 上门取件`, rr.order_id]);
202
208
  }
203
209
  catch { }
204
210
  return void res.json({ success: true, status: 'accepted_pickup_pending' });
205
211
  }
206
212
  try {
207
- executeReturnRefund(rr, response);
213
+ executeReturnRefund(rr, response, 'pending');
208
214
  }
209
215
  catch (e) {
210
- const msg = e.message === 'INSUFFICIENT_SELLER_BALANCE' ? '卖家余额不足以退款' : '退款失败';
216
+ const m = e.message;
217
+ if (m === 'RETURN_ALREADY_SETTLED')
218
+ return void res.status(409).json({ error: '该退货已处理(请刷新后查看)' });
219
+ const msg = m === 'INSUFFICIENT_SELLER_BALANCE' ? '卖家余额不足以退款' : '退款失败';
211
220
  return void res.status(400).json({ error: msg });
212
221
  }
213
222
  return void res.json({ success: true, status: 'refunded' });
214
223
  }
215
224
  else {
216
- db.prepare(`UPDATE return_requests SET status = 'rejected', seller_response = ?, resolved_at = datetime('now') WHERE id = ?`)
217
- .run(response, rr.id);
225
+ await dbRun(`UPDATE return_requests SET status = 'rejected', seller_response = ?, resolved_at = datetime('now') WHERE id = ?`, [response, rr.id]);
218
226
  try {
219
- db.prepare(`INSERT INTO return_messages (id, return_id, sender_id, sender_role, body) VALUES (?,?,?,?,?)`)
220
- .run(generateId('rmsg'), rr.id, rr.seller_id, 'seller', `[✗ 拒绝退款] ${response}`);
227
+ await dbRun(`INSERT INTO return_messages (id, return_id, sender_id, sender_role, body) VALUES (?,?,?,?,?)`, [generateId('rmsg'), rr.id, rr.seller_id, 'seller', `[✗ 拒绝退款] ${response}`]);
221
228
  }
222
229
  catch { }
223
230
  try {
224
- db.prepare(`INSERT INTO notifications (id, user_id, title, body, order_id) VALUES (?,?,?,?,?)`)
225
- .run(generateId('ntf'), rr.buyer_id, '⚠ 退货请求被拒绝', `卖家说明:${response} — 如有异议可发起争议`, rr.order_id);
231
+ await dbRun(`INSERT INTO notifications (id, user_id, title, body, order_id) VALUES (?,?,?,?,?)`, [generateId('ntf'), rr.buyer_id, '⚠ 退货请求被拒绝', `卖家说明:${response} — 如有异议可发起争议`, rr.order_id]);
226
232
  }
227
233
  catch { }
228
234
  return void res.json({ success: true, status: 'rejected' });
229
235
  }
230
236
  });
231
- app.delete('/api/return-requests/:id', (req, res) => {
237
+ app.delete('/api/return-requests/:id', async (req, res) => {
232
238
  const user = auth(req, res);
233
239
  if (!user)
234
240
  return;
235
- const rr = db.prepare(`SELECT id, buyer_id, status FROM return_requests WHERE id = ?`).get(req.params.id);
241
+ const rr = await dbOne(`SELECT id, buyer_id, status FROM return_requests WHERE id = ?`, [req.params.id]);
236
242
  if (!rr)
237
243
  return void res.status(404).json({ error: '不存在' });
238
244
  if (rr.buyer_id !== user.id)
239
245
  return void res.status(403).json({ error: '仅买家可取消' });
240
246
  if (rr.status !== 'pending')
241
247
  return void res.status(400).json({ error: `当前状态 ${rr.status},不可取消` });
242
- db.prepare(`UPDATE return_requests SET status = 'cancelled', resolved_at = datetime('now') WHERE id = ?`).run(rr.id);
248
+ await dbRun(`UPDATE return_requests SET status = 'cancelled', resolved_at = datetime('now') WHERE id = ?`, [rr.id]);
243
249
  res.json({ success: true });
244
250
  });
245
251
  // ─── W2 售后协商时间线 ───────────────────────────────
246
- app.get('/api/return-requests/:id', (req, res) => {
252
+ app.get('/api/return-requests/:id', async (req, res) => {
247
253
  const user = auth(req, res);
248
254
  if (!user)
249
255
  return;
250
- const rr = db.prepare(`
256
+ const rr = await dbOne(`
251
257
  SELECT r.*, p.title as product_title, p.category,
252
258
  o.total_amount as order_total,
253
259
  ub.name as buyer_name, ub.handle as buyer_handle,
@@ -258,17 +264,17 @@ export function registerReturnsRoutes(app, deps) {
258
264
  JOIN users ub ON ub.id = r.buyer_id
259
265
  JOIN users us ON us.id = r.seller_id
260
266
  WHERE r.id = ?
261
- `).get(req.params.id);
267
+ `, [req.params.id]);
262
268
  if (!rr)
263
269
  return void res.status(404).json({ error: '不存在' });
264
270
  if (rr.buyer_id !== user.id && rr.seller_id !== user.id) {
265
271
  return void res.status(403).json({ error: '无权查看' });
266
272
  }
267
- const messages = db.prepare(`
273
+ const messages = await dbAll(`
268
274
  SELECT m.*, u.name as sender_name, u.handle as sender_handle
269
275
  FROM return_messages m LEFT JOIN users u ON u.id = m.sender_id
270
276
  WHERE m.return_id = ? ORDER BY m.created_at ASC
271
- `).all(rr.id);
277
+ `, [rr.id]);
272
278
  const events = [];
273
279
  events.push({
274
280
  id: `create-${rr.id}`,
@@ -332,13 +338,13 @@ export function registerReturnsRoutes(app, deps) {
332
338
  res.json({ item: rr, timeline: events });
333
339
  });
334
340
  // L3 Phase 2: 物流揽收
335
- app.post('/api/return-requests/:id/picked-up', (req, res) => {
341
+ app.post('/api/return-requests/:id/picked-up', async (req, res) => {
336
342
  const user = auth(req, res);
337
343
  if (!user)
338
344
  return;
339
345
  if (user.role !== 'logistics')
340
346
  return void res.status(403).json({ error: '仅物流角色可确认揽收' });
341
- const rr = db.prepare(`SELECT * FROM return_requests WHERE id = ?`).get(req.params.id);
347
+ const rr = await dbOne(`SELECT * FROM return_requests WHERE id = ?`, [req.params.id]);
342
348
  if (!rr)
343
349
  return void res.status(404).json({ error: '退货请求不存在' });
344
350
  if (rr.status !== 'accepted_pickup_pending')
@@ -346,26 +352,24 @@ export function registerReturnsRoutes(app, deps) {
346
352
  const evidence = String(req.body?.evidence || '').trim().slice(0, 500);
347
353
  if (evidence.length < 4)
348
354
  return void res.status(400).json({ error: '请提供揽收证据(快递单号 / GPS / 照片描述)≥ 4 字' });
349
- db.prepare(`UPDATE return_requests SET status = 'picked_up' WHERE id = ?`).run(rr.id);
355
+ await dbRun(`UPDATE return_requests SET status = 'picked_up' WHERE id = ?`, [rr.id]);
350
356
  try {
351
- db.prepare(`INSERT INTO return_messages (id, return_id, sender_id, sender_role, body) VALUES (?,?,?,?,?)`)
352
- .run(generateId('rmsg'), rr.id, user.id, 'logistics', `[📦 已揽收] ${evidence}`);
357
+ await dbRun(`INSERT INTO return_messages (id, return_id, sender_id, sender_role, body) VALUES (?,?,?,?,?)`, [generateId('rmsg'), rr.id, user.id, 'logistics', `[📦 已揽收] ${evidence}`]);
353
358
  }
354
359
  catch { }
355
360
  try {
356
361
  const actions = JSON.stringify([{ kind: 'navigate', label: '处理退货', href: `#returns`, style: 'primary' }]);
357
- db.prepare(`INSERT INTO notifications (id, user_id, type, title, body, order_id, actions) VALUES (?,?,?,?,?,?,?)`)
358
- .run(generateId('ntf'), rr.seller_id, 'return_pickup', '📦 退货已揽收 · 等待你确认收到', `物流已揽收:${evidence.slice(0, 80)}`, rr.order_id, actions);
362
+ await dbRun(`INSERT INTO notifications (id, user_id, type, title, body, order_id, actions) VALUES (?,?,?,?,?,?,?)`, [generateId('ntf'), rr.seller_id, 'return_pickup', '📦 退货已揽收 · 等待你确认收到', `物流已揽收:${evidence.slice(0, 80)}`, rr.order_id, actions]);
359
363
  }
360
364
  catch { }
361
365
  res.json({ success: true, status: 'picked_up' });
362
366
  });
363
367
  // L3 Phase 2: 卖家确认收到 → refunded
364
- app.post('/api/return-requests/:id/received', (req, res) => {
368
+ app.post('/api/return-requests/:id/received', async (req, res) => {
365
369
  const user = auth(req, res);
366
370
  if (!user)
367
371
  return;
368
- const rr = db.prepare(`SELECT * FROM return_requests WHERE id = ?`).get(req.params.id);
372
+ const rr = await dbOne(`SELECT * FROM return_requests WHERE id = ?`, [req.params.id]);
369
373
  if (!rr)
370
374
  return void res.status(404).json({ error: '退货请求不存在' });
371
375
  if (rr.seller_id !== user.id)
@@ -374,21 +378,24 @@ export function registerReturnsRoutes(app, deps) {
374
378
  return void res.status(400).json({ error: `当前状态 ${rr.status},不可确认(应在 picked_up 状态)` });
375
379
  const note = req.body?.note ? String(req.body.note).slice(0, 300) : null;
376
380
  try {
377
- executeReturnRefund(rr, note);
381
+ executeReturnRefund(rr, note, 'picked_up');
378
382
  }
379
383
  catch (e) {
380
- const msg = e.message === 'INSUFFICIENT_SELLER_BALANCE' ? '卖家余额不足以退款' : '退款失败';
384
+ const m = e.message;
385
+ if (m === 'RETURN_ALREADY_SETTLED')
386
+ return void res.status(409).json({ error: '该退货已处理(请刷新后查看)' });
387
+ const msg = m === 'INSUFFICIENT_SELLER_BALANCE' ? '卖家余额不足以退款' : '退款失败';
381
388
  return void res.status(400).json({ error: msg });
382
389
  }
383
390
  res.json({ success: true, status: 'refunded' });
384
391
  });
385
- app.get('/api/logistics/return-pickups', (req, res) => {
392
+ app.get('/api/logistics/return-pickups', async (req, res) => {
386
393
  const user = auth(req, res);
387
394
  if (!user)
388
395
  return;
389
396
  if (user.role !== 'logistics')
390
397
  return void res.status(403).json({ error: '仅物流角色' });
391
- const rows = db.prepare(`
398
+ const rows = await dbAll(`
392
399
  SELECT rr.id, rr.order_id, rr.product_id, rr.refund_amount, rr.pickup_address,
393
400
  rr.reason, rr.created_at, p.title as product_title,
394
401
  ub.handle as buyer_handle, us.name as seller_name
@@ -398,15 +405,14 @@ export function registerReturnsRoutes(app, deps) {
398
405
  JOIN users us ON us.id = rr.seller_id
399
406
  WHERE rr.status = 'accepted_pickup_pending' AND rr.pickup_requested = 1
400
407
  ORDER BY rr.created_at ASC LIMIT 50
401
- `).all();
408
+ `);
402
409
  res.json({ items: rows });
403
410
  });
404
- app.post('/api/return-requests/:id/messages', (req, res) => {
411
+ app.post('/api/return-requests/:id/messages', async (req, res) => {
405
412
  const user = auth(req, res);
406
413
  if (!user)
407
414
  return;
408
- const rr = db.prepare(`SELECT id, buyer_id, seller_id, status FROM return_requests WHERE id = ?`)
409
- .get(req.params.id);
415
+ const rr = await dbOne(`SELECT id, buyer_id, seller_id, status FROM return_requests WHERE id = ?`, [req.params.id]);
410
416
  if (!rr)
411
417
  return void res.status(404).json({ error: '不存在' });
412
418
  const isBuyer = rr.buyer_id === user.id;
@@ -421,14 +427,12 @@ export function registerReturnsRoutes(app, deps) {
421
427
  return void res.status(400).json({ error: '消息长度 1-1000 字' });
422
428
  const reasons = detectFraud(body);
423
429
  const mid = generateId('rmsg');
424
- db.prepare(`INSERT INTO return_messages (id, return_id, sender_id, sender_role, body, flagged, flag_reasons) VALUES (?,?,?,?,?,?,?)`)
425
- .run(mid, rr.id, user.id, isBuyer ? 'buyer' : 'seller', body, reasons.length ? 1 : 0, reasons.length ? JSON.stringify(reasons) : null);
430
+ await dbRun(`INSERT INTO return_messages (id, return_id, sender_id, sender_role, body, flagged, flag_reasons) VALUES (?,?,?,?,?,?,?)`, [mid, rr.id, user.id, isBuyer ? 'buyer' : 'seller', body, reasons.length ? 1 : 0, reasons.length ? JSON.stringify(reasons) : null]);
426
431
  try {
427
432
  const otherId = isBuyer ? rr.seller_id : rr.buyer_id;
428
433
  const orderId = rr.order_id;
429
434
  const actions = JSON.stringify([{ kind: 'navigate', label: '查看协商', href: `#order/${orderId}`, style: 'primary' }]);
430
- db.prepare(`INSERT INTO notifications (id, user_id, type, title, body, order_id, actions) VALUES (?,?,?,?,?,?,?)`)
431
- .run(generateId('ntf'), otherId, 'return_msg', '💬 退货协商新消息', body.slice(0, 80), orderId, actions);
435
+ await dbRun(`INSERT INTO notifications (id, user_id, type, title, body, order_id, actions) VALUES (?,?,?,?,?,?,?)`, [generateId('ntf'), otherId, 'return_msg', '💬 退货协商新消息', body.slice(0, 80), orderId, actions]);
432
436
  }
433
437
  catch (e) {
434
438
  console.warn('[notif return_msg]', e.message);
@@ -436,11 +440,11 @@ export function registerReturnsRoutes(app, deps) {
436
440
  res.json({ success: true, id: mid, flagged: reasons.length > 0, flag_reasons: reasons });
437
441
  });
438
442
  // buyer 升级到争议(仅 rejected 后或 pending ≥ 7 天)
439
- app.post('/api/return-requests/:id/escalate', (req, res) => {
443
+ app.post('/api/return-requests/:id/escalate', async (req, res) => {
440
444
  const user = auth(req, res);
441
445
  if (!user)
442
446
  return;
443
- const rr = db.prepare(`SELECT * FROM return_requests WHERE id = ?`).get(req.params.id);
447
+ const rr = await dbOne(`SELECT * FROM return_requests WHERE id = ?`, [req.params.id]);
444
448
  if (!rr)
445
449
  return void res.status(404).json({ error: '不存在' });
446
450
  if (rr.buyer_id !== user.id)
@@ -456,7 +460,7 @@ export function registerReturnsRoutes(app, deps) {
456
460
  }
457
461
  if (rr.escalated_dispute_id)
458
462
  return void res.status(400).json({ error: '已升级' });
459
- const order = db.prepare('SELECT id, total_amount FROM orders WHERE id = ?').get(rr.order_id);
463
+ const order = await dbOne('SELECT id, total_amount FROM orders WHERE id = ?', [rr.order_id]);
460
464
  if (!order)
461
465
  return void res.status(500).json({ error: '订单数据缺失' });
462
466
  const reason = `退货协商失败:${RETURN_REASON_DEFAULT_LABEL[String(rr.reason)] || rr.reason}${rr.reason_text ? ' — ' + rr.reason_text : ''}`;
@@ -480,8 +484,7 @@ export function registerReturnsRoutes(app, deps) {
480
484
  return void res.status(500).json({ error: '升级失败:' + e.message });
481
485
  }
482
486
  try {
483
- db.prepare(`INSERT INTO notifications (id, user_id, title, body, order_id) VALUES (?,?,?,?,?)`)
484
- .run(generateId('ntf'), rr.seller_id, '⚖️ 退货已升级为争议', `争议 ${disputeId} 已创建,请在 48h 内提交反驳`, rr.order_id);
487
+ await dbRun(`INSERT INTO notifications (id, user_id, title, body, order_id) VALUES (?,?,?,?,?)`, [generateId('ntf'), rr.seller_id, '⚖️ 退货已升级为争议', `争议 ${disputeId} 已创建,请在 48h 内提交反驳`, rr.order_id]);
485
488
  }
486
489
  catch { }
487
490
  try {
@@ -1,8 +1,12 @@
1
+ import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
1
2
  export function registerReviewsRoutes(app, deps) {
3
+ // 只读/单写站点走 RFC-016 异步 seam(dbOne/dbAll/dbRun)。
4
+ // db 保留:claim 是质押/escrow 资金路径(dup 门 + 钱包扣减 + INSERT 任务必须原子),
5
+ // 用 db.transaction 同步事务守恒;Phase 3 随资金路径迁 pg(BEGIN + SELECT...FOR UPDATE)。
2
6
  const { db, auth, isTrustedRole, errorRes, generateId, REVIEW_CLAIM_TARGETS, REVIEW_CLAIM_STAKE, REVIEW_CLAIM_DEADLINE_HOURS, REVIEW_VERIFIERS_NEEDED } = deps;
3
- app.get('/api/reviews/recent', (req, res) => {
7
+ app.get('/api/reviews/recent', async (req, res) => {
4
8
  const limit = Math.min(100, Math.max(10, Number(req.query.limit) || 50));
5
- const items = db.prepare(`
9
+ const items = await dbAll(`
6
10
  SELECT s.id, s.external_url, s.external_platform, s.thumbnail_url, s.title, s.click_count, s.like_count,
7
11
  s.created_at, s.related_product_id,
8
12
  u.handle as owner_handle, u.name as owner_name,
@@ -12,10 +16,10 @@ export function registerReviewsRoutes(app, deps) {
12
16
  LEFT JOIN products p ON p.id = s.related_product_id AND p.status = 'active'
13
17
  WHERE s.status = 'active'
14
18
  ORDER BY s.created_at DESC LIMIT ?
15
- `).all(limit);
19
+ `, [limit]);
16
20
  res.json({ items });
17
21
  });
18
- app.post('/api/reviews/:type/:id/claim', (req, res) => {
22
+ app.post('/api/reviews/:type/:id/claim', async (req, res) => {
19
23
  const user = auth(req, res);
20
24
  if (!user)
21
25
  return;
@@ -29,14 +33,14 @@ export function registerReviewsRoutes(app, deps) {
29
33
  let productId = null;
30
34
  // #1017 fix: shareables / manifest_registry 实际列名是 related_product_id
31
35
  if (reviewType === 'shareable') {
32
- const row = db.prepare('SELECT owner_id, related_product_id FROM shareables WHERE id = ?').get(req.params.id);
36
+ const row = await dbOne('SELECT owner_id, related_product_id FROM shareables WHERE id = ?', [req.params.id]);
33
37
  if (!row)
34
38
  return void res.status(404).json({ error: '评测不存在' });
35
39
  reviewerId = row.owner_id;
36
40
  productId = row.related_product_id;
37
41
  }
38
42
  else {
39
- const row = db.prepare('SELECT owner_id, related_product_id FROM manifest_registry WHERE hash = ?').get(req.params.id);
43
+ const row = await dbOne('SELECT owner_id, related_product_id FROM manifest_registry WHERE hash = ?', [req.params.id]);
40
44
  if (!row)
41
45
  return void res.status(404).json({ error: '原生评测不存在' });
42
46
  reviewerId = row.owner_id;
@@ -51,31 +55,50 @@ export function registerReviewsRoutes(app, deps) {
51
55
  if (text.length < 6 || text.length > 500)
52
56
  return void res.status(400).json({ error: 'claim_text 长度需 6-500 字' });
53
57
  const evidence = req.body?.evidence_uri ? String(req.body.evidence_uri).trim().slice(0, 500) : null;
54
- const wallet = db.prepare('SELECT balance FROM wallets WHERE user_id = ?').get(user.id);
58
+ // 友好预检查(读):余额不足直接早退;真正的守恒门在下面的事务内(WHERE balance >= stake)
59
+ const wallet = await dbOne('SELECT balance FROM wallets WHERE user_id = ?', [user.id]);
55
60
  if (!wallet || wallet.balance < REVIEW_CLAIM_STAKE) {
56
61
  return void res.status(400).json({ error: `余额不足:发起需锁 ${REVIEW_CLAIM_STAKE} WAZ` });
57
62
  }
58
- const dup = db.prepare(`SELECT id FROM review_claim_tasks WHERE review_type = ? AND review_id = ? AND claimant_id = ? AND claim_target = ? AND status = 'open'`)
59
- .get(reviewType, req.params.id, user.id, target);
60
- if (dup)
61
- return void res.status(409).json({ error: '你已对此评测同一项发起过 open 声明' });
62
63
  const id = generateId('rct');
63
64
  const deadline = new Date(Date.now() + REVIEW_CLAIM_DEADLINE_HOURS * 3600_000).toISOString();
64
- db.prepare(`INSERT INTO review_claim_tasks (id, review_type, review_id, product_id, reviewer_id, claimant_id, claim_target, claim_text, evidence_uri, stake_claimant, deadline_at, status) VALUES (?,?,?,?,?,?,?,?,?,?,?,'open')`)
65
- .run(id, reviewType, req.params.id, productId, reviewerId, user.id, target, text, evidence, REVIEW_CLAIM_STAKE, deadline);
66
- db.prepare('UPDATE wallets SET balance = balance - ?, escrowed = escrowed + ? WHERE user_id = ?')
67
- .run(REVIEW_CLAIM_STAKE, REVIEW_CLAIM_STAKE, user.id);
65
+ // 质押/escrow 原子段(同步事务):dup + 钱包扣减(守恒 guard)+ INSERT 任务,
66
+ // 任一失败整段回滚 不会出现"任务已建但钱没锁"或"双重 open 声明"或透支。
67
+ try {
68
+ db.transaction(() => {
69
+ const dup = db.prepare(`SELECT id FROM review_claim_tasks WHERE review_type = ? AND review_id = ? AND claimant_id = ? AND claim_target = ? AND status = 'open'`)
70
+ .get(reviewType, req.params.id, user.id, target);
71
+ if (dup)
72
+ throw new Error('CLAIM_DUP');
73
+ // 守恒:仅当余额仍 >= stake 才扣(挡并发透支);changes=0 → 回滚
74
+ const debit = db.prepare('UPDATE wallets SET balance = balance - ?, escrowed = escrowed + ? WHERE user_id = ? AND balance >= ?')
75
+ .run(REVIEW_CLAIM_STAKE, REVIEW_CLAIM_STAKE, user.id, REVIEW_CLAIM_STAKE);
76
+ if (debit.changes === 0)
77
+ throw new Error('CLAIM_INSUFFICIENT');
78
+ db.prepare(`INSERT INTO review_claim_tasks (id, review_type, review_id, product_id, reviewer_id, claimant_id, claim_target, claim_text, evidence_uri, stake_claimant, deadline_at, status) VALUES (?,?,?,?,?,?,?,?,?,?,?,'open')`)
79
+ .run(id, reviewType, req.params.id, productId, reviewerId, user.id, target, text, evidence, REVIEW_CLAIM_STAKE, deadline);
80
+ })();
81
+ }
82
+ catch (e) {
83
+ const msg = e.message;
84
+ if (msg === 'CLAIM_DUP')
85
+ return void res.status(409).json({ error: '你已对此评测同一项发起过 open 声明' });
86
+ if (msg === 'CLAIM_INSUFFICIENT')
87
+ return void res.status(400).json({ error: `余额不足:发起需锁 ${REVIEW_CLAIM_STAKE} WAZ` });
88
+ console.error('[reviews claim tx]', msg);
89
+ return void res.status(500).json({ error: '发起声明失败,请重试' });
90
+ }
68
91
  res.json({ success: true, claim_id: id, deadline_at: deadline, stake_locked: REVIEW_CLAIM_STAKE });
69
92
  });
70
- app.get('/api/reviews/:type/:id/claims', (req, res) => {
71
- const rows = db.prepare(`
93
+ app.get('/api/reviews/:type/:id/claims', async (req, res) => {
94
+ const rows = await dbAll(`
72
95
  SELECT rct.id, rct.claim_target, rct.claim_text, rct.evidence_uri, rct.status, rct.ruling, rct.deadline_at, rct.resolved_at, rct.created_at,
73
96
  u.name as claimant_name,
74
97
  (SELECT COUNT(*) FROM review_claim_votes WHERE claim_id = rct.id) as votes_count
75
98
  FROM review_claim_tasks rct JOIN users u ON u.id = rct.claimant_id
76
99
  WHERE rct.review_type = ? AND rct.review_id = ?
77
100
  ORDER BY rct.created_at DESC LIMIT 50
78
- `).all(req.params.type, req.params.id);
101
+ `, [req.params.type, req.params.id]);
79
102
  res.json({ claims: rows, votes_needed: REVIEW_VERIFIERS_NEEDED });
80
103
  });
81
104
  }
@@ -1,4 +1,5 @@
1
1
  import { createHash } from 'node:crypto';
2
+ import { dbOne } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
2
3
  function sha256_hex(s) {
3
4
  return createHash('sha256').update(s).digest('hex');
4
5
  }
@@ -8,14 +9,14 @@ export function registerRewardsApplyRoutes(app, deps) {
8
9
  return sha256_hex(`rewards_apply|consent_version=${consentVersion}|user=${userId}|page_loaded_at=${pageLoadedAt}`);
9
10
  }
10
11
  // GET /api/rewards/status — current state + escrow tally
11
- app.get('/api/rewards/status', (req, res) => {
12
+ app.get('/api/rewards/status', async (req, res) => {
12
13
  const user = auth(req, res);
13
14
  if (!user)
14
15
  return;
15
16
  const userId = user.id;
16
- const optIn = db.prepare("SELECT rewards_opted_in FROM users WHERE id = ?").get(userId)?.rewards_opted_in ?? 0;
17
- const lastAction = db.prepare("SELECT action, created_at FROM rewards_applications WHERE user_id = ? ORDER BY created_at DESC LIMIT 1").get(userId);
18
- const currentMajor = db.prepare("SELECT version, hash, change_class, effective_at, text_zh, text_en FROM rewards_consent_texts WHERE change_class='major' ORDER BY effective_at DESC LIMIT 1").get();
17
+ const optIn = (await dbOne("SELECT rewards_opted_in FROM users WHERE id = ?", [userId]))?.rewards_opted_in ?? 0;
18
+ const lastAction = (await dbOne("SELECT action, created_at FROM rewards_applications WHERE user_id = ? ORDER BY created_at DESC LIMIT 1", [userId]));
19
+ const currentMajor = await dbOne("SELECT version, hash, change_class, effective_at, text_zh, text_en FROM rewards_consent_texts WHERE change_class='major' ORDER BY effective_at DESC LIMIT 1", []);
19
20
  let state;
20
21
  if (optIn === 1)
21
22
  state = 'opted_in';
@@ -25,8 +26,8 @@ export function registerRewardsApplyRoutes(app, deps) {
25
26
  state = 'auto_downgraded';
26
27
  else
27
28
  state = 'never_activated';
28
- const completedOrders = db.prepare("SELECT COUNT(*) AS n FROM orders WHERE buyer_id = ? AND status = 'completed'").get(userId).n;
29
- const passkeyCount = db.prepare("SELECT COUNT(*) AS n FROM webauthn_credentials WHERE user_id = ?").get(userId).n;
29
+ const completedOrders = (await dbOne("SELECT COUNT(*) AS n FROM orders WHERE buyer_id = ? AND status = 'completed'", [userId])).n;
30
+ const passkeyCount = (await dbOne("SELECT COUNT(*) AS n FROM webauthn_credentials WHERE user_id = ?", [userId])).n;
30
31
  const minOrders = Number(getProtocolParam('rewards_opt_in.min_completed_orders', 1));
31
32
  const requirePasskey = Number(getProtocolParam('rewards_opt_in.require_passkey', 1));
32
33
  const delaySec = Number(getProtocolParam('rewards_opt_in.consent_delay_seconds', 8));
@@ -35,8 +36,8 @@ export function registerRewardsApplyRoutes(app, deps) {
35
36
  missing.push(`completed_orders ${completedOrders}/${minOrders}`);
36
37
  if (requirePasskey === 1 && passkeyCount === 0)
37
38
  missing.push('passkey_not_registered');
38
- const pending = db.prepare("SELECT COUNT(*) AS n, COALESCE(SUM(amount),0) AS total FROM pending_commission_escrow WHERE recipient_user_id = ? AND status = 'pending'").get(userId);
39
- const expired = db.prepare("SELECT COUNT(*) AS n, COALESCE(SUM(amount),0) AS total FROM pending_commission_escrow WHERE recipient_user_id = ? AND status = 'expired'").get(userId);
39
+ const pending = (await dbOne("SELECT COUNT(*) AS n, COALESCE(SUM(amount),0) AS total FROM pending_commission_escrow WHERE recipient_user_id = ? AND status = 'pending'", [userId]));
40
+ const expired = (await dbOne("SELECT COUNT(*) AS n, COALESCE(SUM(amount),0) AS total FROM pending_commission_escrow WHERE recipient_user_id = ? AND status = 'expired'", [userId]));
40
41
  res.json({
41
42
  state,
42
43
  opted_in: optIn === 1,
@@ -60,7 +61,7 @@ export function registerRewardsApplyRoutes(app, deps) {
60
61
  });
61
62
  });
62
63
  // POST /api/rewards/apply — activate (or reconfirm) opt-in + drain escrow
63
- app.post('/api/rewards/apply', (req, res) => {
64
+ app.post('/api/rewards/apply', async (req, res) => {
64
65
  const user = auth(req, res);
65
66
  if (!user)
66
67
  return;
@@ -71,11 +72,11 @@ export function registerRewardsApplyRoutes(app, deps) {
71
72
  const page_loaded_at = Number(body.page_loaded_at || 0);
72
73
  const webauthn_token = body.webauthn_token ? String(body.webauthn_token) : undefined;
73
74
  // 1. Verify currently opted-out
74
- const optIn = db.prepare("SELECT rewards_opted_in FROM users WHERE id = ?").get(userId)?.rewards_opted_in ?? 0;
75
+ const optIn = (await dbOne("SELECT rewards_opted_in FROM users WHERE id = ?", [userId]))?.rewards_opted_in ?? 0;
75
76
  if (optIn === 1)
76
77
  return void errorRes(res, 409, 'ALREADY_OPTED_IN', '已 opted-in,无需重复申请');
77
78
  // 2. Verify consent version matches current major
78
- const currentMajor = db.prepare("SELECT version, hash FROM rewards_consent_texts WHERE change_class='major' ORDER BY effective_at DESC LIMIT 1").get();
79
+ const currentMajor = await dbOne("SELECT version, hash FROM rewards_consent_texts WHERE change_class='major' ORDER BY effective_at DESC LIMIT 1", []);
79
80
  if (!currentMajor)
80
81
  return void errorRes(res, 500, 'NO_CONSENT_TEXT', 'rewards_consent_texts 未 seed,无法申请');
81
82
  if (consent_version !== currentMajor.version) {
@@ -102,11 +103,11 @@ export function registerRewardsApplyRoutes(app, deps) {
102
103
  }
103
104
  // 5. Pre-conditions (re-check inside server)
104
105
  const minOrders = Number(getProtocolParam('rewards_opt_in.min_completed_orders', 1));
105
- const completedOrders = db.prepare("SELECT COUNT(*) AS n FROM orders WHERE buyer_id = ? AND status = 'completed'").get(userId).n;
106
+ const completedOrders = (await dbOne("SELECT COUNT(*) AS n FROM orders WHERE buyer_id = ? AND status = 'completed'", [userId])).n;
106
107
  if (completedOrders < minOrders)
107
108
  return void errorRes(res, 403, 'INSUFFICIENT_ORDERS', `需 ${minOrders} 笔已完成订单,目前 ${completedOrders}`);
108
109
  const requirePasskey = Number(getProtocolParam('rewards_opt_in.require_passkey', 1));
109
- const passkeyCount = db.prepare("SELECT COUNT(*) AS n FROM webauthn_credentials WHERE user_id = ?").get(userId).n;
110
+ const passkeyCount = (await dbOne("SELECT COUNT(*) AS n FROM webauthn_credentials WHERE user_id = ?", [userId])).n;
110
111
  if (requirePasskey === 1 && passkeyCount === 0)
111
112
  return void errorRes(res, 403, 'PASSKEY_REQUIRED', '需先注册 Passkey');
112
113
  // 6. Atomic: consume Passkey gate + insert audit + flip flag + drain escrow → wallet
@@ -165,14 +166,14 @@ export function registerRewardsApplyRoutes(app, deps) {
165
166
  res.json({ ok: true, state: 'opted_in', drained_from_escrow: drained });
166
167
  });
167
168
  // POST /api/rewards/deactivate — flip off; subsequent commissions → charity
168
- app.post('/api/rewards/deactivate', (req, res) => {
169
+ app.post('/api/rewards/deactivate', async (req, res) => {
169
170
  const user = auth(req, res);
170
171
  if (!user)
171
172
  return;
172
173
  const userId = user.id;
173
174
  const body = req.body || {};
174
175
  const webauthn_token = body.webauthn_token ? String(body.webauthn_token) : undefined;
175
- const optIn = db.prepare("SELECT rewards_opted_in FROM users WHERE id = ?").get(userId)?.rewards_opted_in ?? 0;
176
+ const optIn = (await dbOne("SELECT rewards_opted_in FROM users WHERE id = ?", [userId]))?.rewards_opted_in ?? 0;
176
177
  if (optIn === 0)
177
178
  return void errorRes(res, 409, 'ALREADY_OPTED_OUT', '本来就未 opted-in,无需关闭');
178
179
  const requirePasskey = Number(getProtocolParam('rewards_opt_in.require_passkey', 1));