@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,4 +1,7 @@
1
+ import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
1
2
  export function registerArbitratorRoutes(app, deps) {
3
+ // 只读/单写站点走 RFC-016 异步 seam;db 保留:apply/withdraw/approve/reject 是
4
+ // stake 资金路径,状态翻转 + 钱包扣/退必须原子(db.transaction + CAS),Phase 3 迁 pg 行锁。
2
5
  const { db, generateId, auth, requireArbitrationAdmin, checkArbitratorEligibility, getArbitratorState, errorRes, logAdminAction, ARB_STAKE_REQUIRED, ARB_APP_REJECT_COOLDOWN_DAYS, } = deps;
3
6
  app.get('/api/arbitrator/eligibility', (req, res) => {
4
7
  const user = auth(req, res);
@@ -12,7 +15,7 @@ export function registerArbitratorRoutes(app, deps) {
12
15
  return;
13
16
  res.json(getArbitratorState(user.id));
14
17
  });
15
- app.post('/api/arbitrator/apply', (req, res) => {
18
+ app.post('/api/arbitrator/apply', async (req, res) => {
16
19
  const user = auth(req, res);
17
20
  if (!user)
18
21
  return;
@@ -20,13 +23,13 @@ export function registerArbitratorRoutes(app, deps) {
20
23
  return void errorRes(res, 403, 'ROLE_NOT_BUYER', '外部仲裁员仅 buyer 角色可申请(卖家 / 受信角色请联系管理员)');
21
24
  }
22
25
  const userId = user.id;
23
- const wl = db.prepare("SELECT 1 FROM arbitrator_whitelist WHERE user_id = ?").get(userId);
26
+ const wl = await dbOne("SELECT 1 FROM arbitrator_whitelist WHERE user_id = ?", [userId]);
24
27
  if (wl)
25
28
  return void res.json({ error: '你已经是仲裁员,无需重新申请' });
26
- const pending = db.prepare("SELECT 1 FROM arbitrator_applications WHERE user_id = ? AND status = 'pending'").get(userId);
29
+ const pending = await dbOne("SELECT 1 FROM arbitrator_applications WHERE user_id = ? AND status = 'pending'", [userId]);
27
30
  if (pending)
28
31
  return void res.json({ error: '你已有待审申请' });
29
- const lastReject = db.prepare("SELECT reviewed_at FROM arbitrator_applications WHERE user_id = ? AND status = 'rejected' ORDER BY reviewed_at DESC LIMIT 1").get(userId);
32
+ const lastReject = await dbOne("SELECT reviewed_at FROM arbitrator_applications WHERE user_id = ? AND status = 'rejected' ORDER BY reviewed_at DESC LIMIT 1", [userId]);
30
33
  if (lastReject?.reviewed_at) {
31
34
  const cooldownEnd = new Date(new Date(lastReject.reviewed_at).getTime() + ARB_APP_REJECT_COOLDOWN_DAYS * 86400_000);
32
35
  if (cooldownEnd > new Date()) {
@@ -36,80 +39,143 @@ export function registerArbitratorRoutes(app, deps) {
36
39
  const elig = checkArbitratorEligibility(userId);
37
40
  if (!elig.eligible)
38
41
  return void res.json({ error: '信誉指标未达标', eligibility: elig });
42
+ // 友好预检查(读):真正的守恒门在事务内(WHERE balance >= stake)。
39
43
  if (ARB_STAKE_REQUIRED > 0) {
40
- const wallet = db.prepare("SELECT balance, staked FROM wallets WHERE user_id = ?").get(userId);
44
+ const wallet = await dbOne("SELECT balance FROM wallets WHERE user_id = ?", [userId]);
41
45
  if (!wallet || wallet.balance < ARB_STAKE_REQUIRED) {
42
46
  return void res.json({ error: `质押需 ${ARB_STAKE_REQUIRED} WAZ,钱包余额不足` });
43
47
  }
44
- db.prepare("UPDATE wallets SET balance = balance - ?, staked = staked + ? WHERE user_id = ?")
45
- .run(ARB_STAKE_REQUIRED, ARB_STAKE_REQUIRED, userId);
46
48
  }
47
- db.prepare("INSERT INTO arbitrator_applications (id, user_id, status, snapshot) VALUES (?,?,?,?)")
48
- .run(generateId('aapp'), userId, 'pending', JSON.stringify(elig.items));
49
+ const appId = generateId('aapp');
50
+ // stake 原子段:重检 whitelist/pending(防并发双申请双质押)+ 钱包扣减(守恒 guard)+ INSERT 申请
51
+ try {
52
+ db.transaction(() => {
53
+ if (db.prepare("SELECT 1 FROM arbitrator_whitelist WHERE user_id = ?").get(userId))
54
+ throw new Error('ARB_ALREADY');
55
+ if (db.prepare("SELECT 1 FROM arbitrator_applications WHERE user_id = ? AND status = 'pending'").get(userId))
56
+ throw new Error('ARB_PENDING');
57
+ if (ARB_STAKE_REQUIRED > 0) {
58
+ const debit = db.prepare("UPDATE wallets SET balance = balance - ?, staked = staked + ? WHERE user_id = ? AND balance >= ?")
59
+ .run(ARB_STAKE_REQUIRED, ARB_STAKE_REQUIRED, userId, ARB_STAKE_REQUIRED);
60
+ if (debit.changes === 0)
61
+ throw new Error('ARB_INSUFFICIENT');
62
+ }
63
+ db.prepare("INSERT INTO arbitrator_applications (id, user_id, status, snapshot) VALUES (?,?,?,?)")
64
+ .run(appId, userId, 'pending', JSON.stringify(elig.items));
65
+ })();
66
+ }
67
+ catch (e) {
68
+ const msg = e.message;
69
+ if (msg === 'ARB_ALREADY')
70
+ return void res.json({ error: '你已经是仲裁员,无需重新申请' });
71
+ if (msg === 'ARB_PENDING')
72
+ return void res.json({ error: '你已有待审申请' });
73
+ if (msg === 'ARB_INSUFFICIENT')
74
+ return void res.json({ error: `质押需 ${ARB_STAKE_REQUIRED} WAZ,钱包余额不足` });
75
+ console.error('[arbitrator apply tx]', msg);
76
+ return void res.status(500).json({ error: '申请失败,请重试' });
77
+ }
49
78
  res.json({ success: true, stake_locked: ARB_STAKE_REQUIRED });
50
79
  });
51
- app.post('/api/arbitrator/withdraw-application', (req, res) => {
80
+ app.post('/api/arbitrator/withdraw-application', async (req, res) => {
52
81
  const user = auth(req, res);
53
82
  if (!user)
54
83
  return;
55
84
  const userId = user.id;
56
- const pending = db.prepare("SELECT id FROM arbitrator_applications WHERE user_id = ? AND status = 'pending' LIMIT 1").get(userId);
85
+ const pending = await dbOne("SELECT id FROM arbitrator_applications WHERE user_id = ? AND status = 'pending' LIMIT 1", [userId]);
57
86
  if (!pending)
58
87
  return void res.json({ error: '没有待审申请' });
59
- db.prepare("UPDATE arbitrator_applications SET status='withdrawn', reviewed_at=datetime('now') WHERE id = ?").run(pending.id);
60
- if (ARB_STAKE_REQUIRED > 0) {
61
- db.prepare("UPDATE wallets SET balance = balance + ?, staked = staked - ? WHERE user_id = ?")
62
- .run(ARB_STAKE_REQUIRED, ARB_STAKE_REQUIRED, userId);
88
+ // 原子段:CAS 翻转 pending→withdrawn(防并发/admin 抢跑双退)+ 退质押仅在本请求真翻转时
89
+ try {
90
+ db.transaction(() => {
91
+ const cas = db.prepare("UPDATE arbitrator_applications SET status='withdrawn', reviewed_at=datetime('now') WHERE id = ? AND status = 'pending'").run(pending.id);
92
+ if (cas.changes === 0)
93
+ throw new Error('ARB_RACE');
94
+ if (ARB_STAKE_REQUIRED > 0) {
95
+ db.prepare("UPDATE wallets SET balance = balance + ?, staked = staked - ? WHERE user_id = ?").run(ARB_STAKE_REQUIRED, ARB_STAKE_REQUIRED, userId);
96
+ }
97
+ })();
98
+ }
99
+ catch (e) {
100
+ if (e.message === 'ARB_RACE')
101
+ return void res.status(409).json({ error: '申请状态已变化,请刷新' });
102
+ console.error('[arbitrator withdraw tx]', e.message);
103
+ return void res.status(500).json({ error: '撤回失败,请重试' });
63
104
  }
64
105
  res.json({ success: true });
65
106
  });
66
107
  // Admin
67
- app.get('/api/admin/arbitrator-applications', (req, res) => {
108
+ app.get('/api/admin/arbitrator-applications', async (req, res) => {
68
109
  const admin = requireArbitrationAdmin(req, res);
69
110
  if (!admin)
70
111
  return;
71
112
  const status = String(req.query.status || 'pending');
72
- const items = db.prepare(`
113
+ const items = await dbAll(`
73
114
  SELECT aa.*, u.name as user_name, u.handle, u.region
74
115
  FROM arbitrator_applications aa
75
116
  JOIN users u ON u.id = aa.user_id
76
117
  WHERE aa.status = ?
77
118
  ORDER BY aa.applied_at DESC LIMIT 100
78
- `).all(status);
119
+ `, [status]);
79
120
  res.json({ items });
80
121
  });
81
- app.post('/api/admin/arbitrator-applications/:id/approve', (req, res) => {
122
+ app.post('/api/admin/arbitrator-applications/:id/approve', async (req, res) => {
82
123
  const admin = requireArbitrationAdmin(req, res);
83
124
  if (!admin)
84
125
  return;
85
126
  const { note } = req.body;
86
- const appRow = db.prepare("SELECT id, user_id, status FROM arbitrator_applications WHERE id = ?").get(req.params.id);
127
+ const appRow = await dbOne("SELECT id, user_id, status FROM arbitrator_applications WHERE id = ?", [req.params.id]);
87
128
  if (!appRow)
88
129
  return void res.json({ error: '申请不存在' });
89
130
  if (appRow.status !== 'pending')
90
131
  return void res.json({ error: '该申请不在待审状态' });
91
- db.prepare("UPDATE arbitrator_applications SET status='approved', reviewed_at=datetime('now'), reviewed_by=?, decision_note=? WHERE id=?")
92
- .run(admin.id, note || null, appRow.id);
93
- db.prepare(`INSERT OR REPLACE INTO arbitrator_whitelist (user_id, note, is_system, granted_by, stake_amount) VALUES (?,?,0,?,?)`)
94
- .run(appRow.user_id, note || '外部仲裁员批准', admin.id, ARB_STAKE_REQUIRED);
132
+ // 原子段:CAS 翻转 pending→approved + 入白名单仅在本请求真翻转时(防并发双批准)
133
+ try {
134
+ db.transaction(() => {
135
+ const cas = db.prepare("UPDATE arbitrator_applications SET status='approved', reviewed_at=datetime('now'), reviewed_by=?, decision_note=? WHERE id=? AND status='pending'")
136
+ .run(admin.id, note || null, appRow.id);
137
+ if (cas.changes === 0)
138
+ throw new Error('ARB_RACE');
139
+ db.prepare(`INSERT OR REPLACE INTO arbitrator_whitelist (user_id, note, is_system, granted_by, stake_amount) VALUES (?,?,0,?,?)`)
140
+ .run(appRow.user_id, note || '外部仲裁员批准', admin.id, ARB_STAKE_REQUIRED);
141
+ })();
142
+ }
143
+ catch (e) {
144
+ if (e.message === 'ARB_RACE')
145
+ return void res.json({ error: '该申请不在待审状态' });
146
+ console.error('[arbitrator approve tx]', e.message);
147
+ return void res.status(500).json({ error: '批准失败,请重试' });
148
+ }
95
149
  logAdminAction(admin.id, 'approve_arbitrator', 'user', appRow.user_id, { note });
96
150
  res.json({ success: true });
97
151
  });
98
- app.post('/api/admin/arbitrator-applications/:id/reject', (req, res) => {
152
+ app.post('/api/admin/arbitrator-applications/:id/reject', async (req, res) => {
99
153
  const admin = requireArbitrationAdmin(req, res);
100
154
  if (!admin)
101
155
  return;
102
156
  const { note } = req.body;
103
- const appRow = db.prepare("SELECT id, user_id, status FROM arbitrator_applications WHERE id = ?").get(req.params.id);
157
+ const appRow = await dbOne("SELECT id, user_id, status FROM arbitrator_applications WHERE id = ?", [req.params.id]);
104
158
  if (!appRow)
105
159
  return void res.json({ error: '申请不存在' });
106
160
  if (appRow.status !== 'pending')
107
161
  return void res.json({ error: '该申请不在待审状态' });
108
- db.prepare("UPDATE arbitrator_applications SET status='rejected', reviewed_at=datetime('now'), reviewed_by=?, decision_note=? WHERE id=?")
109
- .run(admin.id, note || null, appRow.id);
110
- if (ARB_STAKE_REQUIRED > 0) {
111
- db.prepare("UPDATE wallets SET balance = balance + ?, staked = staked - ? WHERE user_id = ?")
112
- .run(ARB_STAKE_REQUIRED, ARB_STAKE_REQUIRED, appRow.user_id);
162
+ // 原子段:CAS 翻转 pending→rejected + 退质押仅在本请求真翻转时(防并发双拒双退)
163
+ try {
164
+ db.transaction(() => {
165
+ const cas = db.prepare("UPDATE arbitrator_applications SET status='rejected', reviewed_at=datetime('now'), reviewed_by=?, decision_note=? WHERE id=? AND status='pending'")
166
+ .run(admin.id, note || null, appRow.id);
167
+ if (cas.changes === 0)
168
+ throw new Error('ARB_RACE');
169
+ if (ARB_STAKE_REQUIRED > 0) {
170
+ db.prepare("UPDATE wallets SET balance = balance + ?, staked = staked - ? WHERE user_id = ?").run(ARB_STAKE_REQUIRED, ARB_STAKE_REQUIRED, appRow.user_id);
171
+ }
172
+ })();
173
+ }
174
+ catch (e) {
175
+ if (e.message === 'ARB_RACE')
176
+ return void res.json({ error: '该申请不在待审状态' });
177
+ console.error('[arbitrator reject tx]', e.message);
178
+ return void res.status(500).json({ error: '拒绝失败,请重试' });
113
179
  }
114
180
  logAdminAction(admin.id, 'reject_arbitrator', 'user', appRow.user_id, { note });
115
181
  res.json({ success: true });