@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,7 +1,9 @@
1
+ import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
1
2
  export function registerAdminUsersQueryRoutes(app, deps) {
2
- const { db, requireUsersAdmin, adminCanOperateOn, isRootAdmin, isAllowedSponsor, maskApiKey, computeLightTags, getAdminScope, getSellerDailyLimit, todayStartISO, broadcastSystemEvent, INTERNAL_AUDITOR_ID } = deps;
3
+ // db 已全量走 RFC-016 异步 seam(dbOne/dbAll/dbRun),不再直接用 deps.db
4
+ const { requireUsersAdmin, adminCanOperateOn, isRootAdmin, isAllowedSponsor, maskApiKey, computeLightTags, getAdminScope, getSellerDailyLimit, todayStartISO, broadcastSystemEvent, INTERNAL_AUDITOR_ID, logAdminAction } = deps;
3
5
  // P1-1: 按 handle / id 任意角色查找
4
- app.get('/api/admin/users/lookup', (req, res) => {
6
+ app.get('/api/admin/users/lookup', async (req, res) => {
5
7
  const admin = requireUsersAdmin(req, res);
6
8
  if (!admin)
7
9
  return;
@@ -9,15 +11,15 @@ export function registerAdminUsersQueryRoutes(app, deps) {
9
11
  if (!raw)
10
12
  return void res.status(400).json({ error: 'q 必填(user_id 或 handle)' });
11
13
  const term = raw.replace(/^@/, '');
12
- let user = db.prepare("SELECT id, name, handle, role, created_at FROM users WHERE handle = ? AND id NOT IN ('sys_protocol', ?)").get(term, INTERNAL_AUDITOR_ID);
14
+ let user = await dbOne("SELECT id, name, handle, role, created_at FROM users WHERE handle = ? AND id NOT IN ('sys_protocol', ?)", [term, INTERNAL_AUDITOR_ID]);
13
15
  if (!user)
14
- user = db.prepare("SELECT id, name, handle, role, created_at FROM users WHERE id = ? AND id NOT IN ('sys_protocol', ?)").get(term, INTERNAL_AUDITOR_ID);
16
+ user = await dbOne("SELECT id, name, handle, role, created_at FROM users WHERE id = ? AND id NOT IN ('sys_protocol', ?)", [term, INTERNAL_AUDITOR_ID]);
15
17
  if (!user)
16
18
  return void res.status(404).json({ error: '用户不存在' });
17
19
  res.json({ user });
18
20
  });
19
21
  // Wave F-3: 完整事件流
20
- app.get('/api/admin/users/:id/timeline', (req, res) => {
22
+ app.get('/api/admin/users/:id/timeline', async (req, res) => {
21
23
  const admin = requireUsersAdmin(req, res);
22
24
  if (!admin)
23
25
  return;
@@ -29,52 +31,52 @@ export function registerAdminUsersQueryRoutes(app, deps) {
29
31
  return;
30
32
  events.push({ ts, type, icon, summary, ref_id: refId || null, ref_type: refType || null, amount: amount ?? null });
31
33
  };
32
- db.prepare(`SELECT id, status, total_amount, created_at, buyer_id, seller_id, logistics_id FROM orders WHERE buyer_id=? OR seller_id=? OR logistics_id=? ORDER BY created_at DESC LIMIT 100`).all(id, id, id).forEach(o => {
34
+ (await dbAll(`SELECT id, status, total_amount, created_at, buyer_id, seller_id, logistics_id FROM orders WHERE buyer_id=? OR seller_id=? OR logistics_id=? ORDER BY created_at DESC LIMIT 100`, [id, id, id])).forEach(o => {
33
35
  const role = o.buyer_id === id ? '买家' : o.seller_id === id ? '卖家' : '物流';
34
36
  push(o.created_at, 'order', '📦', `订单 (${role}) ${o.id} · ${o.total_amount} WAZ · ${o.status}`, o.id, 'order', o.total_amount);
35
37
  });
36
- db.prepare(`SELECT order_id, stars, comment, created_at, buyer_id, seller_id FROM order_ratings WHERE buyer_id=? OR seller_id=? ORDER BY created_at DESC LIMIT 50`).all(id, id).forEach(r => {
38
+ (await dbAll(`SELECT order_id, stars, comment, created_at, buyer_id, seller_id FROM order_ratings WHERE buyer_id=? OR seller_id=? ORDER BY created_at DESC LIMIT 50`, [id, id])).forEach(r => {
37
39
  const role = r.buyer_id === id ? '给出' : '收到';
38
40
  push(r.created_at, 'rating', '⭐', `${role} ${r.stars} 星评价 (订单 ${r.order_id})`, r.order_id, 'order');
39
41
  });
40
- db.prepare(`SELECT id, order_id, reason, refund_amount, status, created_at, buyer_id, seller_id FROM return_requests WHERE buyer_id=? OR seller_id=? ORDER BY created_at DESC LIMIT 50`).all(id, id).forEach(r => {
42
+ (await dbAll(`SELECT id, order_id, reason, refund_amount, status, created_at, buyer_id, seller_id FROM return_requests WHERE buyer_id=? OR seller_id=? ORDER BY created_at DESC LIMIT 50`, [id, id])).forEach(r => {
41
43
  const role = r.buyer_id === id ? '发起' : '收到';
42
44
  push(r.created_at, 'return', '↩', `${role} 退货 (${r.status}, ${r.refund_amount} WAZ, ${r.reason})`, r.order_id, 'order', r.refund_amount);
43
45
  });
44
- db.prepare(`SELECT id, category, subject, status, created_at FROM feedback_tickets WHERE user_id=? ORDER BY created_at DESC LIMIT 30`).all(id).forEach(f => {
46
+ (await dbAll(`SELECT id, category, subject, status, created_at FROM feedback_tickets WHERE user_id=? ORDER BY created_at DESC LIMIT 30`, [id])).forEach(f => {
45
47
  push(f.created_at, 'feedback', '💬', `反馈 (${f.category}/${f.status}): ${f.subject}`, f.id, 'feedback');
46
48
  });
47
- db.prepare(`SELECT checkin_date, reward, streak, created_at FROM daily_checkins WHERE user_id=? ORDER BY created_at DESC LIMIT 30`).all(id).forEach(c => {
49
+ (await dbAll(`SELECT checkin_date, reward, streak, created_at FROM daily_checkins WHERE user_id=? ORDER BY created_at DESC LIMIT 30`, [id])).forEach(c => {
48
50
  push(c.created_at, 'checkin', '📅', `签到 ${c.checkin_date} · streak ${c.streak} · +${c.reward} WAZ`, null, null, c.reward);
49
51
  });
50
- db.prepare(`SELECT task_key, reward, claimed_at FROM task_completions WHERE user_id=? AND claimed_at IS NOT NULL ORDER BY claimed_at DESC LIMIT 30`).all(id).forEach(tc => {
52
+ (await dbAll(`SELECT task_key, reward, claimed_at FROM task_completions WHERE user_id=? AND claimed_at IS NOT NULL ORDER BY claimed_at DESC LIMIT 30`, [id])).forEach(tc => {
51
53
  push(tc.claimed_at, 'task', '🎁', `任务 ${tc.task_key} 领取 +${tc.reward} WAZ`, null, null, tc.reward);
52
54
  });
53
- db.prepare(`SELECT amount, source, ref, created_at FROM platform_reward_log WHERE user_id=? ORDER BY created_at DESC LIMIT 50`).all(id).forEach(p => {
55
+ (await dbAll(`SELECT amount, source, ref, created_at FROM platform_reward_log WHERE user_id=? ORDER BY created_at DESC LIMIT 50`, [id])).forEach(p => {
54
56
  push(p.created_at, 'reward', '💰', `平台拨付 (${p.source}${p.ref ? '/' + p.ref : ''}) +${p.amount} WAZ`, null, null, p.amount);
55
57
  });
56
- db.prepare(`SELECT followee_id, created_at FROM follows WHERE follower_id=? ORDER BY created_at DESC LIMIT 20`).all(id).forEach(f => {
58
+ (await dbAll(`SELECT followee_id, created_at FROM follows WHERE follower_id=? ORDER BY created_at DESC LIMIT 20`, [id])).forEach(f => {
57
59
  push(f.created_at, 'follow', '🤝', `关注 ${f.followee_id}`, f.followee_id, 'user');
58
60
  });
59
- db.prepare(`SELECT product_id, created_at FROM user_wishlist WHERE user_id=? ORDER BY created_at DESC LIMIT 20`).all(id).forEach(w => {
61
+ (await dbAll(`SELECT product_id, created_at FROM user_wishlist WHERE user_id=? ORDER BY created_at DESC LIMIT 20`, [id])).forEach(w => {
60
62
  push(w.created_at, 'wishlist', '❤', `加入心愿单 ${w.product_id}`, w.product_id, 'product');
61
63
  });
62
- db.prepare(`SELECT product_id, created_at FROM product_waitlist WHERE user_id=? ORDER BY created_at DESC LIMIT 20`).all(id).forEach(w => {
64
+ (await dbAll(`SELECT product_id, created_at FROM product_waitlist WHERE user_id=? ORDER BY created_at DESC LIMIT 20`, [id])).forEach(w => {
63
65
  push(w.created_at, 'waitlist', '⏰', `加入补货提醒 ${w.product_id}`, w.product_id, 'product');
64
66
  });
65
- db.prepare(`SELECT id, status, ruling_type, created_at, resolved_at, initiator_id, defendant_id FROM disputes WHERE initiator_id=? OR defendant_id=? ORDER BY created_at DESC LIMIT 30`).all(id, id).forEach(d => {
67
+ (await dbAll(`SELECT id, status, ruling_type, created_at, resolved_at, initiator_id, defendant_id FROM disputes WHERE initiator_id=? OR defendant_id=? ORDER BY created_at DESC LIMIT 30`, [id, id])).forEach(d => {
66
68
  const role = d.initiator_id === id ? '发起' : '被诉';
67
69
  push(d.created_at, 'dispute_open', '⚖', `${role} 争议 ${d.id} (${d.status})`, d.id, 'dispute');
68
70
  if (d.resolved_at)
69
71
  push(d.resolved_at, 'dispute_resolved', '⚖', `争议 ${d.id} 结案 (${d.ruling_type || '—'})`, d.id, 'dispute');
70
72
  });
71
- const userRow = db.prepare(`SELECT created_at, name, role FROM users WHERE id=?`).get(id);
73
+ const userRow = await dbOne(`SELECT created_at, name, role FROM users WHERE id=?`, [id]);
72
74
  if (userRow)
73
75
  push(userRow.created_at, 'register', '🎉', `注册账号 ${userRow.name} (${userRow.role})`, null, null);
74
76
  events.sort((a, b) => String(b.ts).localeCompare(String(a.ts)));
75
77
  res.json({ items: events.slice(0, limit), total: events.length });
76
78
  });
77
- app.post('/api/admin/users/batch-action', (req, res) => {
79
+ app.post('/api/admin/users/batch-action', async (req, res) => {
78
80
  const admin = requireUsersAdmin(req, res);
79
81
  if (!admin)
80
82
  return;
@@ -94,13 +96,12 @@ export function registerAdminUsersQueryRoutes(app, deps) {
94
96
  continue;
95
97
  }
96
98
  if (action === 'suspend') {
97
- db.prepare(`INSERT INTO user_moderation (user_id, suspended, reason, suspended_by, suspended_at)
99
+ await dbRun(`INSERT INTO user_moderation (user_id, suspended, reason, suspended_by, suspended_at)
98
100
  VALUES (?, 1, ?, ?, datetime('now'))
99
- ON CONFLICT(user_id) DO UPDATE SET suspended = 1, reason = excluded.reason, suspended_by = excluded.suspended_by, suspended_at = excluded.suspended_at`)
100
- .run(uid, reasonStr, admin.id);
101
+ ON CONFLICT(user_id) DO UPDATE SET suspended = 1, reason = excluded.reason, suspended_by = excluded.suspended_by, suspended_at = excluded.suspended_at`, [uid, reasonStr, admin.id]);
101
102
  }
102
103
  else {
103
- db.prepare(`UPDATE user_moderation SET suspended = 0 WHERE user_id = ?`).run(uid);
104
+ await dbRun(`UPDATE user_moderation SET suspended = 0 WHERE user_id = ?`, [uid]);
104
105
  }
105
106
  results.push({ user_id: uid, status: 'ok' });
106
107
  }
@@ -109,13 +110,24 @@ export function registerAdminUsersQueryRoutes(app, deps) {
109
110
  }
110
111
  }
111
112
  const ok = results.filter(r => r.status === 'ok').length;
113
+ try {
114
+ // 防审计行膨胀:批量上限本就 ≤200,但仍只记前 50 个 id + 计数(truncated 标记),保证单行有界。
115
+ const okIds = results.filter(r => r.status === 'ok').map(r => r.user_id);
116
+ logAdminAction(admin.id, 'users_batch_' + String(action), 'user', null, {
117
+ action, reason: reasonStr, applied: ok, requested: user_ids.length,
118
+ user_ids: okIds.slice(0, 50), user_ids_truncated: okIds.length > 50,
119
+ });
120
+ }
121
+ catch (e) {
122
+ console.error('[users_batch audit]', e);
123
+ }
112
124
  try {
113
125
  broadcastSystemEvent('admin_bulk_' + action, '🛡', `${admin.id} 批量${action === 'suspend' ? '暂停' : '解封'} ${ok} 用户`, null);
114
126
  }
115
127
  catch { }
116
128
  res.json({ success: true, applied: ok, results });
117
129
  });
118
- app.get('/api/admin/users', (req, res) => {
130
+ app.get('/api/admin/users', async (req, res) => {
119
131
  const admin = requireUsersAdmin(req, res);
120
132
  if (!admin)
121
133
  return;
@@ -175,7 +187,7 @@ export function registerAdminUsersQueryRoutes(app, deps) {
175
187
  }
176
188
  }
177
189
  sql += ` ORDER BY u.created_at DESC LIMIT 100`;
178
- const rows = db.prepare(sql).all(...params);
190
+ const rows = await dbAll(sql, params);
179
191
  res.json({
180
192
  match_mode,
181
193
  my_admin_type: admin.admin_type || 'root',
@@ -208,21 +220,21 @@ export function registerAdminUsersQueryRoutes(app, deps) {
208
220
  });
209
221
  });
210
222
  // 完整档案聚合
211
- app.get('/api/admin/users/:id/profile', (req, res) => {
223
+ app.get('/api/admin/users/:id/profile', async (req, res) => {
212
224
  const admin = requireUsersAdmin(req, res);
213
225
  if (!admin)
214
226
  return;
215
227
  if (!adminCanOperateOn(admin, req.params.id, res))
216
228
  return;
217
229
  const id = req.params.id;
218
- const user = db.prepare("SELECT * FROM users WHERE id = ?").get(id);
230
+ const user = await dbOne("SELECT * FROM users WHERE id = ?", [id]);
219
231
  if (!user)
220
232
  return void res.json({ error: '用户不存在' });
221
- const wallet = db.prepare("SELECT balance, staked, escrowed, earned, deposit_address FROM wallets WHERE user_id = ?").get(id);
222
- const mod = db.prepare("SELECT suspended, reason, suspended_by, suspended_at FROM user_moderation WHERE user_id = ?").get(id);
223
- const vw = db.prepare("SELECT tier, daily_quota, tasks_today, quota_reset_at, granted_by, stake_amount, cooldown_until, error_count_180d, is_system, added_at FROM verifier_whitelist WHERE user_id = ?").get(id);
224
- const vs = db.prepare("SELECT verify_rights, tasks_done, tasks_correct, tasks_wrong, suspended_until FROM verifier_stats WHERE user_id = ?").get(id);
225
- const vAppPending = !!db.prepare("SELECT 1 FROM verifier_applications WHERE user_id = ? AND status='pending' LIMIT 1").get(id);
233
+ const wallet = await dbOne("SELECT balance, staked, escrowed, earned, deposit_address FROM wallets WHERE user_id = ?", [id]);
234
+ const mod = await dbOne("SELECT suspended, reason, suspended_by, suspended_at FROM user_moderation WHERE user_id = ?", [id]);
235
+ const vw = await dbOne("SELECT tier, daily_quota, tasks_today, quota_reset_at, granted_by, stake_amount, cooldown_until, error_count_180d, is_system, added_at FROM verifier_whitelist WHERE user_id = ?", [id]);
236
+ const vs = await dbOne("SELECT verify_rights, tasks_done, tasks_correct, tasks_wrong, suspended_until FROM verifier_stats WHERE user_id = ?", [id]);
237
+ const vAppPending = !!(await dbOne("SELECT 1 FROM verifier_applications WHERE user_id = ? AND status='pending' LIMIT 1", [id]));
226
238
  const roleSet = new Set((() => { try {
227
239
  return JSON.parse(user.roles || '[]');
228
240
  }
@@ -231,20 +243,20 @@ export function registerAdminUsersQueryRoutes(app, deps) {
231
243
  } })());
232
244
  const kpis = {};
233
245
  if (roleSet.has('seller')) {
234
- const p = db.prepare(`SELECT COUNT(*) as total,
246
+ const p = (await dbOne(`SELECT COUNT(*) as total,
235
247
  SUM(CASE WHEN status='active' THEN 1 ELSE 0 END) as active,
236
248
  SUM(CASE WHEN status='paused' THEN 1 ELSE 0 END) as paused,
237
249
  SUM(CASE WHEN status='deleted'THEN 1 ELSE 0 END) as deleted
238
- FROM products WHERE seller_id = ?`).get(id);
239
- const o = db.prepare(`SELECT COUNT(*) as total,
250
+ FROM products WHERE seller_id = ?`, [id]));
251
+ const o = (await dbOne(`SELECT COUNT(*) as total,
240
252
  SUM(CASE WHEN status='completed' THEN 1 ELSE 0 END) as completed,
241
253
  COALESCE(SUM(CASE WHEN status='completed' THEN total_amount ELSE 0 END),0) as total_sales
242
- FROM orders WHERE seller_id = ?`).get(id);
243
- const d = db.prepare(`SELECT COUNT(*) as defendant_count,
254
+ FROM orders WHERE seller_id = ?`, [id]));
255
+ const d = (await dbOne(`SELECT COUNT(*) as defendant_count,
244
256
  SUM(CASE WHEN ruling_type IN ('refund_buyer','partial_refund') THEN 1 ELSE 0 END) as lost
245
- FROM disputes WHERE defendant_id = ?`).get(id);
257
+ FROM disputes WHERE defendant_id = ?`, [id]));
246
258
  const today = todayStartISO();
247
- const todayCount = db.prepare("SELECT COUNT(*) as n FROM products WHERE seller_id = ? AND created_at >= ?").get(id, today).n;
259
+ const todayCount = (await dbOne("SELECT COUNT(*) as n FROM products WHERE seller_id = ? AND created_at >= ?", [id, today])).n;
248
260
  const dailyLimit = getSellerDailyLimit({ id, created_at: user.created_at });
249
261
  kpis.seller = {
250
262
  products_total: p.total, products_active: p.active, products_paused: p.paused, products_deleted: p.deleted,
@@ -257,20 +269,20 @@ export function registerAdminUsersQueryRoutes(app, deps) {
257
269
  };
258
270
  }
259
271
  if (roleSet.has('buyer')) {
260
- const o = db.prepare(`SELECT COUNT(*) as total,
272
+ const o = (await dbOne(`SELECT COUNT(*) as total,
261
273
  SUM(CASE WHEN status='completed' THEN 1 ELSE 0 END) as completed,
262
274
  COALESCE(SUM(CASE WHEN status='completed' THEN total_amount ELSE 0 END),0) as total_spent,
263
275
  MAX(created_at) as last_order_at
264
- FROM orders WHERE buyer_id = ?`).get(id);
276
+ FROM orders WHERE buyer_id = ?`, [id]));
265
277
  kpis.buyer = {
266
278
  orders_total: o.total, orders_completed: o.completed,
267
279
  total_spent: o.total_spent, last_order_at: o.last_order_at,
268
280
  };
269
281
  }
270
282
  if (roleSet.has('logistics')) {
271
- const o = db.prepare(`SELECT COUNT(*) as total,
283
+ const o = (await dbOne(`SELECT COUNT(*) as total,
272
284
  SUM(CASE WHEN status='completed' THEN 1 ELSE 0 END) as completed
273
- FROM orders WHERE logistics_id = ?`).get(id);
285
+ FROM orders WHERE logistics_id = ?`, [id]));
274
286
  kpis.logistics = { deliveries_total: o.total, deliveries_completed: o.completed };
275
287
  }
276
288
  if (roleSet.has('verifier') && vw) {
@@ -296,28 +308,28 @@ export function registerAdminUsersQueryRoutes(app, deps) {
296
308
  return;
297
309
  events.push({ ts, type, icon, summary, ref_id: refId, ref_type: refType });
298
310
  };
299
- db.prepare(`SELECT id, total_amount, product_id, status, created_at FROM orders WHERE buyer_id = ? ORDER BY created_at DESC LIMIT 10`).all(id).forEach(o => pushEvt(o.created_at, 'order_buy', '🛒', `下单 ${o.total_amount} WAZ (${o.status})`, o.id, 'order'));
300
- db.prepare(`SELECT id, total_amount, status, created_at FROM orders WHERE seller_id = ? ORDER BY created_at DESC LIMIT 10`).all(id).forEach(o => pushEvt(o.created_at, 'order_sell', '💰', `售出 ${o.total_amount} WAZ (${o.status})`, o.id, 'order'));
301
- db.prepare(`SELECT id, title, status, created_at FROM products WHERE seller_id = ? ORDER BY created_at DESC LIMIT 10`).all(id).forEach(p => pushEvt(p.created_at, 'product_listed', '🏪', `上架商品 ${(p.title || '').slice(0, 30)}`, p.id, 'product'));
302
- db.prepare(`SELECT id, task_id, verdict, claimed_at, submitted_at FROM verify_submissions WHERE verifier_id = ? ORDER BY claimed_at DESC LIMIT 10`).all(id).forEach(s => {
311
+ (await dbAll(`SELECT id, total_amount, product_id, status, created_at FROM orders WHERE buyer_id = ? ORDER BY created_at DESC LIMIT 10`, [id])).forEach(o => pushEvt(o.created_at, 'order_buy', '🛒', `下单 ${o.total_amount} WAZ (${o.status})`, o.id, 'order'));
312
+ (await dbAll(`SELECT id, total_amount, status, created_at FROM orders WHERE seller_id = ? ORDER BY created_at DESC LIMIT 10`, [id])).forEach(o => pushEvt(o.created_at, 'order_sell', '💰', `售出 ${o.total_amount} WAZ (${o.status})`, o.id, 'order'));
313
+ (await dbAll(`SELECT id, title, status, created_at FROM products WHERE seller_id = ? ORDER BY created_at DESC LIMIT 10`, [id])).forEach(p => pushEvt(p.created_at, 'product_listed', '🏪', `上架商品 ${(p.title || '').slice(0, 30)}`, p.id, 'product'));
314
+ (await dbAll(`SELECT id, task_id, verdict, claimed_at, submitted_at FROM verify_submissions WHERE verifier_id = ? ORDER BY claimed_at DESC LIMIT 10`, [id])).forEach(s => {
303
315
  pushEvt(s.claimed_at, 'verify_claimed', '🔍', `认领验证任务`, s.task_id, 'task');
304
316
  if (s.submitted_at) {
305
317
  const icon = s.verdict === 'correct' ? '✓' : s.verdict === 'wrong' ? '✗' : '⏳';
306
318
  pushEvt(s.submitted_at, 'verify_submitted', icon, `提交验证 (${s.verdict || 'pending'})`, s.task_id, 'task');
307
319
  }
308
320
  });
309
- db.prepare(`SELECT id, status, ruling_type, created_at, resolved_at, initiator_id, defendant_id FROM disputes WHERE initiator_id = ? OR defendant_id = ? ORDER BY created_at DESC LIMIT 10`).all(id, id).forEach(d => {
321
+ (await dbAll(`SELECT id, status, ruling_type, created_at, resolved_at, initiator_id, defendant_id FROM disputes WHERE initiator_id = ? OR defendant_id = ? ORDER BY created_at DESC LIMIT 10`, [id, id])).forEach(d => {
310
322
  const role = d.initiator_id === id ? '发起' : '被告';
311
323
  pushEvt(d.created_at, 'dispute_init', '⚖', `争议 ${role} (${d.status})`, d.id, 'dispute');
312
324
  if (d.resolved_at)
313
325
  pushEvt(d.resolved_at, 'dispute_resolved', '⚖', `争议结案 (${d.ruling_type || '—'})`, d.id, 'dispute');
314
326
  });
315
- db.prepare(`SELECT id, status, decision_note, applied_at, reviewed_at FROM verifier_applications WHERE user_id = ? ORDER BY applied_at DESC LIMIT 5`).all(id).forEach(a => {
327
+ (await dbAll(`SELECT id, status, decision_note, applied_at, reviewed_at FROM verifier_applications WHERE user_id = ? ORDER BY applied_at DESC LIMIT 5`, [id])).forEach(a => {
316
328
  pushEvt(a.applied_at, 'verifier_apply', '📥', `提交审核员申请`, a.id, 'verifier_app');
317
329
  if (a.reviewed_at)
318
330
  pushEvt(a.reviewed_at, 'verifier_review', a.status === 'approved' ? '✅' : '❌', `申请${a.status === 'approved' ? '获批' : a.status === 'rejected' ? '被拒' : a.status}`, a.id, 'verifier_app');
319
331
  });
320
- db.prepare(`SELECT id, status, created_at, reviewed_at FROM verifier_appeals WHERE user_id = ? ORDER BY created_at DESC LIMIT 5`).all(id).forEach(a => {
332
+ (await dbAll(`SELECT id, status, created_at, reviewed_at FROM verifier_appeals WHERE user_id = ? ORDER BY created_at DESC LIMIT 5`, [id])).forEach(a => {
321
333
  pushEvt(a.created_at, 'appeal_submitted', '📩', `提交申诉`, a.id, 'appeal');
322
334
  if (a.reviewed_at)
323
335
  pushEvt(a.reviewed_at, 'appeal_decided', a.status === 'accepted' ? '✅' : '❌', `申诉${a.status === 'accepted' ? '成立' : '驳回'}`, a.id, 'appeal');
@@ -332,10 +344,10 @@ export function registerAdminUsersQueryRoutes(app, deps) {
332
344
  if (user.locked_until && new Date(user.locked_until).getTime() > Date.now()) {
333
345
  risks.push({ severity: 'high', label: '账户已锁定', detail: `解锁: ${user.locked_until}` });
334
346
  }
335
- const openDisputes = db.prepare("SELECT COUNT(*) as n FROM disputes WHERE defendant_id = ? AND status IN ('open','in_review')").get(id).n;
347
+ const openDisputes = (await dbOne("SELECT COUNT(*) as n FROM disputes WHERE defendant_id = ? AND status IN ('open','in_review')", [id])).n;
336
348
  if (openDisputes > 0)
337
349
  risks.push({ severity: 'medium', label: '未结争议作为被告', detail: `${openDisputes} 起` });
338
- const lostCount = db.prepare("SELECT COUNT(*) as n FROM disputes WHERE defendant_id = ? AND ruling_type IN ('refund_buyer','partial_refund')").get(id).n;
350
+ const lostCount = (await dbOne("SELECT COUNT(*) as n FROM disputes WHERE defendant_id = ? AND ruling_type IN ('refund_buyer','partial_refund')", [id])).n;
339
351
  if (lostCount > 0)
340
352
  risks.push({ severity: 'low', label: '历史仲裁判输', detail: `${lostCount} 次` });
341
353
  if (wallet && Number(wallet.staked) > Number(wallet.balance) * 2) {
@@ -345,14 +357,14 @@ export function registerAdminUsersQueryRoutes(app, deps) {
345
357
  const ec = Number(vw.error_count_180d);
346
358
  risks.push({ severity: ec >= 2 ? 'high' : 'medium', label: '审核员近期错误', detail: `180 天 ${ec} 次` });
347
359
  }
348
- const audit = db.prepare(`
360
+ const audit = (await dbAll(`
349
361
  SELECT al.id, al.admin_id, al.action, al.detail, al.created_at,
350
362
  u.name as admin_name
351
363
  FROM admin_audit_log al
352
364
  LEFT JOIN users u ON u.id = al.admin_id
353
365
  WHERE al.target_type = 'user' AND al.target_id = ?
354
366
  ORDER BY al.created_at DESC LIMIT 20
355
- `).all(id).map(r => ({
367
+ `, [id])).map(r => ({
356
368
  ...r,
357
369
  detail: r.detail ? (() => { try {
358
370
  return JSON.parse(r.detail);
@@ -1,18 +1,21 @@
1
+ import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
1
2
  export function registerAdminVerifierFlowRoutes(app, deps) {
3
+ // 只读站点走 RFC-016 异步 seam;db 保留:approve/reject/decide 含状态翻转 + 退质押 + 补发奖励,
4
+ // 必须原子(db.transaction + CAS 翻转,退款/奖励仅在本请求真翻转时落),Phase 3 迁 pg 行锁。
2
5
  const { db, requireVerifierMgmtAdmin, TIER_QUOTAS, VERIFIER_STAKE_REQUIRED, todayStartISO, logAdminAction } = deps;
3
- app.get('/api/admin/verifier-applications', (req, res) => {
6
+ app.get('/api/admin/verifier-applications', async (req, res) => {
4
7
  const admin = requireVerifierMgmtAdmin(req, res);
5
8
  if (!admin)
6
9
  return;
7
10
  const status = req.query.status || 'pending';
8
- const rows = db.prepare(`
11
+ const rows = await dbAll(`
9
12
  SELECT va.id, va.user_id, va.status, va.applied_at, va.reviewed_at, va.reviewed_by, va.decision_note, va.snapshot,
10
13
  u.name as user_name, u.email
11
14
  FROM verifier_applications va
12
15
  LEFT JOIN users u ON u.id = va.user_id
13
16
  WHERE va.status = ?
14
17
  ORDER BY va.applied_at DESC LIMIT 100
15
- `).all(status);
18
+ `, [status]);
16
19
  res.json({
17
20
  applications: rows.map(r => ({
18
21
  ...r,
@@ -25,59 +28,83 @@ export function registerAdminVerifierFlowRoutes(app, deps) {
25
28
  })),
26
29
  });
27
30
  });
28
- app.post('/api/admin/verifier-applications/:id/approve', (req, res) => {
31
+ app.post('/api/admin/verifier-applications/:id/approve', async (req, res) => {
29
32
  const admin = requireVerifierMgmtAdmin(req, res);
30
33
  if (!admin)
31
34
  return;
32
35
  const { tier, note } = req.body;
33
36
  const validTier = ['trial-1', 'trial-2', 'trial-3', 'active-1', 'active-2'].includes(tier) ? tier : 'trial-1';
34
- const apl = db.prepare("SELECT id, user_id, status FROM verifier_applications WHERE id = ?").get(req.params.id);
37
+ const apl = await dbOne("SELECT id, user_id, status FROM verifier_applications WHERE id = ?", [req.params.id]);
35
38
  if (!apl)
36
39
  return void res.json({ error: '申请不存在' });
37
40
  if (apl.status !== 'pending')
38
41
  return void res.json({ error: '该申请不在待审状态' });
39
- db.prepare("UPDATE verifier_applications SET status='approved', reviewed_at=datetime('now'), reviewed_by=?, decision_note=? WHERE id=?")
40
- .run(admin.id, note || null, apl.id);
41
- db.prepare(`INSERT OR REPLACE INTO verifier_whitelist
42
- (user_id, note, tier, daily_quota, tasks_today, quota_reset_at, granted_by, stake_amount, is_system)
43
- VALUES (?,?,?,?,0,?,?,?,0)`)
44
- .run(apl.user_id, note || `批准为 ${validTier}`, validTier, TIER_QUOTAS[validTier], todayStartISO(), admin.id, VERIFIER_STAKE_REQUIRED);
45
- db.prepare("INSERT OR IGNORE INTO verifier_stats (user_id) VALUES (?)").run(apl.user_id);
42
+ // 原子段:CAS 翻转 pending→approved + 入白名单 + stats(防并发双批准)
43
+ try {
44
+ db.transaction(() => {
45
+ const cas = db.prepare("UPDATE verifier_applications SET status='approved', reviewed_at=datetime('now'), reviewed_by=?, decision_note=? WHERE id=? AND status='pending'")
46
+ .run(admin.id, note || null, apl.id);
47
+ if (cas.changes === 0)
48
+ throw new Error('APP_RACE');
49
+ db.prepare(`INSERT OR REPLACE INTO verifier_whitelist
50
+ (user_id, note, tier, daily_quota, tasks_today, quota_reset_at, granted_by, stake_amount, is_system)
51
+ VALUES (?,?,?,?,0,?,?,?,0)`)
52
+ .run(apl.user_id, note || `批准为 ${validTier}`, validTier, TIER_QUOTAS[validTier], todayStartISO(), admin.id, VERIFIER_STAKE_REQUIRED);
53
+ db.prepare("INSERT OR IGNORE INTO verifier_stats (user_id) VALUES (?)").run(apl.user_id);
54
+ })();
55
+ }
56
+ catch (e) {
57
+ if (e.message === 'APP_RACE')
58
+ return void res.json({ error: '该申请不在待审状态' });
59
+ console.error('[verifier approve tx]', e.message);
60
+ return void res.status(500).json({ error: '批准失败,请重试' });
61
+ }
46
62
  logAdminAction(admin.id, 'approve_verifier', 'user', apl.user_id, { tier: validTier, note });
47
63
  res.json({ success: true });
48
64
  });
49
- app.post('/api/admin/verifier-applications/:id/reject', (req, res) => {
65
+ app.post('/api/admin/verifier-applications/:id/reject', async (req, res) => {
50
66
  const admin = requireVerifierMgmtAdmin(req, res);
51
67
  if (!admin)
52
68
  return;
53
69
  const { note } = req.body;
54
- const apl = db.prepare("SELECT id, user_id, status FROM verifier_applications WHERE id = ?").get(req.params.id);
70
+ const apl = await dbOne("SELECT id, user_id, status FROM verifier_applications WHERE id = ?", [req.params.id]);
55
71
  if (!apl)
56
72
  return void res.json({ error: '申请不存在' });
57
73
  if (apl.status !== 'pending')
58
74
  return void res.json({ error: '该申请不在待审状态' });
59
- db.prepare("UPDATE verifier_applications SET status='rejected', reviewed_at=datetime('now'), reviewed_by=?, decision_note=? WHERE id=?")
60
- .run(admin.id, note || null, apl.id);
61
- // 退回质押
62
- if (VERIFIER_STAKE_REQUIRED > 0) {
63
- db.prepare("UPDATE wallets SET balance = balance + ?, staked = staked - ? WHERE user_id = ?")
64
- .run(VERIFIER_STAKE_REQUIRED, VERIFIER_STAKE_REQUIRED, apl.user_id);
75
+ // 原子段:CAS 翻转 pending→rejected + 退质押仅在本请求真翻转时(防并发双拒双退)
76
+ try {
77
+ db.transaction(() => {
78
+ const cas = db.prepare("UPDATE verifier_applications SET status='rejected', reviewed_at=datetime('now'), reviewed_by=?, decision_note=? WHERE id=? AND status='pending'")
79
+ .run(admin.id, note || null, apl.id);
80
+ if (cas.changes === 0)
81
+ throw new Error('APP_RACE');
82
+ if (VERIFIER_STAKE_REQUIRED > 0) {
83
+ db.prepare("UPDATE wallets SET balance = balance + ?, staked = staked - ? WHERE user_id = ?").run(VERIFIER_STAKE_REQUIRED, VERIFIER_STAKE_REQUIRED, apl.user_id);
84
+ }
85
+ })();
86
+ }
87
+ catch (e) {
88
+ if (e.message === 'APP_RACE')
89
+ return void res.json({ error: '该申请不在待审状态' });
90
+ console.error('[verifier reject tx]', e.message);
91
+ return void res.status(500).json({ error: '拒绝失败,请重试' });
65
92
  }
66
93
  logAdminAction(admin.id, 'reject_verifier', 'user', apl.user_id, { note });
67
94
  res.json({ success: true });
68
95
  });
69
- app.get('/api/admin/verifier-appeals', (req, res) => {
96
+ app.get('/api/admin/verifier-appeals', async (req, res) => {
70
97
  const admin = requireVerifierMgmtAdmin(req, res);
71
98
  if (!admin)
72
99
  return;
73
100
  const status = req.query.status || 'pending';
74
- const rows = db.prepare(`
101
+ const rows = await dbAll(`
75
102
  SELECT va.id, va.user_id, va.task_id, va.submission_id, va.reason, va.evidence_urls, va.status,
76
103
  va.admin_note, va.reviewed_by, va.reviewed_at, va.created_at, u.name as user_name
77
104
  FROM verifier_appeals va LEFT JOIN users u ON u.id = va.user_id
78
105
  WHERE va.status = ?
79
106
  ORDER BY va.created_at DESC LIMIT 100
80
- `).all(status);
107
+ `, [status]);
81
108
  res.json({
82
109
  appeals: rows.map(r => ({
83
110
  ...r,
@@ -90,35 +117,49 @@ export function registerAdminVerifierFlowRoutes(app, deps) {
90
117
  })),
91
118
  });
92
119
  });
93
- app.post('/api/admin/verifier-appeals/:id/decide', (req, res) => {
120
+ app.post('/api/admin/verifier-appeals/:id/decide', async (req, res) => {
94
121
  const admin = requireVerifierMgmtAdmin(req, res);
95
122
  if (!admin)
96
123
  return;
97
124
  const { decision, note } = req.body; // 'accepted' | 'rejected'
98
125
  if (!['accepted', 'rejected'].includes(decision))
99
126
  return void res.json({ error: 'decision 无效' });
100
- const appeal = db.prepare("SELECT id, user_id, status FROM verifier_appeals WHERE id = ?").get(req.params.id);
127
+ const appeal = await dbOne("SELECT id, user_id, status FROM verifier_appeals WHERE id = ?", [req.params.id]);
101
128
  if (!appeal)
102
129
  return void res.json({ error: '申诉不存在' });
103
130
  if (appeal.status !== 'pending')
104
131
  return void res.json({ error: '该申诉已处理' });
105
- db.prepare("UPDATE verifier_appeals SET status = ?, admin_note = ?, reviewed_by = ?, reviewed_at = datetime('now') WHERE id = ?")
106
- .run(decision, note || null, admin.id, appeal.id);
107
- if (decision === 'accepted') {
108
- // 解封 + 验证权 +2 + 错误次数 -1
109
- db.prepare("UPDATE verifier_stats SET suspended_until = NULL, verify_rights = verify_rights + 2 WHERE user_id = ?").run(appeal.user_id);
110
- db.prepare("UPDATE verifier_whitelist SET error_count_180d = MAX(0, error_count_180d - 1) WHERE user_id = ?").run(appeal.user_id);
111
- // 完整重审:翻转该 verifier 在该 task 的 verdict + 补发奖励 + 翻回 stats
112
- const fullAppeal = db.prepare("SELECT task_id FROM verifier_appeals WHERE id = ?").get(appeal.id);
113
- if (fullAppeal?.task_id) {
114
- const sub = db.prepare("SELECT vs.id, vs.verdict, vt.reward_per_verifier FROM verify_submissions vs JOIN verify_tasks vt ON vt.id = vs.task_id WHERE vs.task_id = ? AND vs.verifier_id = ?")
115
- .get(fullAppeal.task_id, appeal.user_id);
116
- if (sub && sub.verdict === 'wrong') {
117
- db.prepare("UPDATE verify_submissions SET verdict = 'correct' WHERE id = ?").run(sub.id);
118
- db.prepare("UPDATE verifier_stats SET tasks_correct = tasks_correct + 1, tasks_wrong = MAX(0, tasks_wrong - 1), verify_rights = verify_rights + 3 WHERE user_id = ?").run(appeal.user_id);
119
- db.prepare("UPDATE wallets SET balance = balance + ? WHERE user_id = ?").run(sub.reward_per_verifier, appeal.user_id);
132
+ // 原子段:CAS 翻转 appeal pending→decision + (accepted )解封/重审/补发奖励 一起落,
133
+ // 防并发双裁决导致 verify_rights 多加、verdict 多翻、奖励多发。
134
+ try {
135
+ db.transaction(() => {
136
+ const cas = db.prepare("UPDATE verifier_appeals SET status = ?, admin_note = ?, reviewed_by = ?, reviewed_at = datetime('now') WHERE id = ? AND status = 'pending'")
137
+ .run(decision, note || null, admin.id, appeal.id);
138
+ if (cas.changes === 0)
139
+ throw new Error('APPEAL_RACE');
140
+ if (decision === 'accepted') {
141
+ // 解封 + 验证权 +2 + 错误次数 -1
142
+ db.prepare("UPDATE verifier_stats SET suspended_until = NULL, verify_rights = verify_rights + 2 WHERE user_id = ?").run(appeal.user_id);
143
+ db.prepare("UPDATE verifier_whitelist SET error_count_180d = MAX(0, error_count_180d - 1) WHERE user_id = ?").run(appeal.user_id);
144
+ // 完整重审:翻转该 verifier 在该 task 的 verdict + 补发奖励 + 翻回 stats
145
+ const fullAppeal = db.prepare("SELECT task_id FROM verifier_appeals WHERE id = ?").get(appeal.id);
146
+ if (fullAppeal?.task_id) {
147
+ const sub = db.prepare("SELECT vs.id, vs.verdict, vt.reward_per_verifier FROM verify_submissions vs JOIN verify_tasks vt ON vt.id = vs.task_id WHERE vs.task_id = ? AND vs.verifier_id = ?")
148
+ .get(fullAppeal.task_id, appeal.user_id);
149
+ if (sub && sub.verdict === 'wrong') {
150
+ db.prepare("UPDATE verify_submissions SET verdict = 'correct' WHERE id = ?").run(sub.id);
151
+ db.prepare("UPDATE verifier_stats SET tasks_correct = tasks_correct + 1, tasks_wrong = MAX(0, tasks_wrong - 1), verify_rights = verify_rights + 3 WHERE user_id = ?").run(appeal.user_id);
152
+ db.prepare("UPDATE wallets SET balance = balance + ? WHERE user_id = ?").run(sub.reward_per_verifier, appeal.user_id);
153
+ }
154
+ }
120
155
  }
121
- }
156
+ })();
157
+ }
158
+ catch (e) {
159
+ if (e.message === 'APPEAL_RACE')
160
+ return void res.json({ error: '该申诉已处理' });
161
+ console.error('[verifier appeal decide tx]', e.message);
162
+ return void res.status(500).json({ error: '裁决失败,请重试' });
122
163
  }
123
164
  logAdminAction(admin.id, 'decide_appeal', 'user', appeal.user_id, { decision, note: note || null });
124
165
  res.json({ success: true });