@seasonkoh/webaz 0.1.24 → 0.1.26

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