@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,6 +1,9 @@
1
1
  import { createHash } from 'crypto';
2
2
  import { getQuestionsForRole, scoreQuiz } from '../data/onboarding-quiz.js';
3
3
  import { getCasesForRole, getCasesForMaintainer, validateCaseReviews } from '../data/onboarding-cases.js';
4
+ // RFC-016 Phase 1 — 申请/题目/案例/申诉的校验读 + 列表读 + 单语句写 → async seam;
5
+ // activate/resign/resolve-appeal 的 3 个角色态 CAS db.transaction 保持同步(Phase 3 迁 pg)。
6
+ import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js';
4
7
  // PR #22 review fix P1-2:披露文本版本(server 与 client 同步,变更披露需 bump)
5
8
  // client 在 src/pwa/public/app.js 必须用相同 version
6
9
  export const GOVERNANCE_APPLY_DISCLOSURE_VERSION = 'v1.0-2026-06-02';
@@ -12,7 +15,7 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
12
15
  function expectedConsentHash(role, userId, pageLoadedAt) {
13
16
  return sha256_hex(`governance_apply|disclosure=${GOVERNANCE_APPLY_DISCLOSURE_VERSION}|role=${role}|user=${userId}|page_loaded_at=${pageLoadedAt}`);
14
17
  }
15
- app.post('/api/governance/onboarding/apply', (req, res) => {
18
+ app.post('/api/governance/onboarding/apply', async (req, res) => {
16
19
  const user = auth(req, res);
17
20
  if (!user)
18
21
  return;
@@ -65,11 +68,11 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
65
68
  return void errorRes(res, 401, 'PASSKEY_INVALID', `Passkey 验证失败: ${result.reason || '未知'}`);
66
69
  }
67
70
  }
68
- const existing = db.prepare(`
71
+ const existing = await dbOne(`
69
72
  SELECT id, status, cooldown_until FROM governance_applications
70
73
  WHERE user_id = ? AND role = ? AND status IN ('pending_onboarding', 'active', 'cooldown')
71
74
  ORDER BY created_at DESC LIMIT 1
72
- `).get(userId, role);
75
+ `, [userId, role]);
73
76
  if (existing) {
74
77
  if (existing.status === 'active') {
75
78
  return void errorRes(res, 409, 'ALREADY_ACTIVE', `你已是 ${role},无需重复申请`);
@@ -100,11 +103,11 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
100
103
  const id = generateId('gapp');
101
104
  const ip = String(req.ip || req.headers['x-forwarded-for'] || 'unknown').split(',')[0].trim();
102
105
  const ua = String(req.headers['user-agent'] || 'unknown');
103
- db.prepare(`
106
+ await dbRun(`
104
107
  INSERT INTO governance_applications
105
108
  (id, user_id, role, action, status, consent_hash, passkey_sig, iron_rule_method, ip_hash, ua_hash)
106
109
  VALUES (?, ?, ?, 'apply', 'pending_onboarding', ?, ?, ?, ?, ?)
107
- `).run(id, userId, role, consent_hash, passkey_sig, iron_rule_method, sha256_16(ip), sha256_16(ua));
110
+ `, [id, userId, role, consent_hash, passkey_sig, iron_rule_method, sha256_16(ip), sha256_16(ua)]);
108
111
  res.json({
109
112
  success: true,
110
113
  application_id: id,
@@ -113,18 +116,18 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
113
116
  note: 'Maintainer 将 review 你的申请。下一步:完成 onboarding 学习 + 案例分析 + 题目(本阶段未上线)',
114
117
  });
115
118
  });
116
- app.get('/api/governance/onboarding/my', (req, res) => {
119
+ app.get('/api/governance/onboarding/my', async (req, res) => {
117
120
  const user = auth(req, res);
118
121
  if (!user)
119
122
  return;
120
123
  const userId = user.id;
121
- const items = db.prepare(`
124
+ const items = await dbAll(`
122
125
  SELECT id, role, action, status, quiz_score, quiz_passed_at, cooldown_until, appeal_reason, appeal_resolution, created_at
123
126
  FROM governance_applications
124
127
  WHERE user_id = ?
125
128
  ORDER BY created_at DESC
126
129
  LIMIT 50
127
- `).all(userId);
130
+ `, [userId]);
128
131
  res.json({ items, count: items.length });
129
132
  });
130
133
  // GET /api/governance/onboarding/quiz?role=arbitrator|verifier
@@ -144,7 +147,7 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
144
147
  // POST /api/governance/onboarding/quiz-submit
145
148
  // 提交题目答案 → server-side scoring → 写 governance_applications.quiz_score
146
149
  // body: { role, answers: [{question_id, answer}] }
147
- app.post('/api/governance/onboarding/quiz-submit', (req, res) => {
150
+ app.post('/api/governance/onboarding/quiz-submit', async (req, res) => {
148
151
  const user = auth(req, res);
149
152
  if (!user)
150
153
  return;
@@ -159,30 +162,35 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
159
162
  return void errorRes(res, 400, 'MISSING_ANSWERS', 'answers 数组为空');
160
163
  }
161
164
  // 找用户的最新 pending_onboarding application(必须先 apply 才能提交 quiz)
162
- const app_ = db.prepare(`
165
+ const app_ = await dbOne(`
163
166
  SELECT id, status FROM governance_applications
164
167
  WHERE user_id = ? AND role = ? AND status = 'pending_onboarding'
165
168
  ORDER BY created_at DESC LIMIT 1
166
- `).get(userId, role);
169
+ `, [userId, role]);
167
170
  if (!app_) {
168
171
  return void errorRes(res, 404, 'NO_PENDING_APPLICATION', `未找到 ${role} 待审申请,请先提交申请`);
169
172
  }
170
173
  // 读 quiz_pass_score(protocol_params,默认 80)
171
- const param = db.prepare("SELECT value FROM protocol_params WHERE key = ?").get('governance_onboarding.quiz_pass_score');
174
+ const param = await dbOne("SELECT value FROM protocol_params WHERE key = ?", ['governance_onboarding.quiz_pass_score']);
172
175
  const passThreshold = param ? Number(param.value) : 80;
173
176
  // 评分
174
177
  const result = scoreQuiz(role, answers, passThreshold);
175
178
  // 更新 quiz_score(只在分数有提升时更新,允许重考)
176
- const existing = db.prepare("SELECT quiz_score, quiz_passed_at FROM governance_applications WHERE id = ?").get(app_.id);
179
+ const existing = (await dbOne("SELECT quiz_score, quiz_passed_at FROM governance_applications WHERE id = ?", [app_.id]));
177
180
  const newScore = result.score_pct;
181
+ // Codex #234 P1:await 预读与写之间 maintainer 可能激活/解决该申请;所有写必须带
182
+ // status='pending_onboarding' 守卫,否则会篡改已离开 pending 的 onboarding audit 证据。
178
183
  if (existing.quiz_score == null || newScore > existing.quiz_score) {
179
- db.prepare("UPDATE governance_applications SET quiz_score = ? WHERE id = ?").run(newScore, app_.id);
184
+ const u = await dbRun("UPDATE governance_applications SET quiz_score = ? WHERE id = ? AND status = 'pending_onboarding'", [newScore, app_.id]);
185
+ if (u.changes === 0)
186
+ return void errorRes(res, 409, 'APPLICATION_MOVED', '申请状态已变更(已被激活/解决),无法更新成绩');
180
187
  }
181
188
  // PR #22 review fix P1-3:quiz pass 推进环节状态(quiz_passed_at 时间戳)
182
189
  // 一旦合格,记录时间戳;后续不变(即便重考更低分,也不抹掉已合格状态)
190
+ // quiz_passed_at IS NULL 守卫保证"只盖一次戳",叠加 status 守卫防离开 pending 后篡改
183
191
  if (result.passed && !existing.quiz_passed_at) {
184
192
  const now = Math.floor(Date.now() / 1000);
185
- db.prepare("UPDATE governance_applications SET quiz_passed_at = ? WHERE id = ?").run(now, app_.id);
193
+ await dbRun("UPDATE governance_applications SET quiz_passed_at = ? WHERE id = ? AND status = 'pending_onboarding' AND quiz_passed_at IS NULL", [now, app_.id]);
186
194
  }
187
195
  res.json({
188
196
  success: true,
@@ -214,7 +222,7 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
214
222
  // body: { role, reviews: [{case_id, chosen_verdict, reasoning}] }
215
223
  // 写 governance_applications.case_review_text(JSON string)
216
224
  // 不立即评分 — maintainer 上岗签字前(阶段 3 #1093)对比 expected_verdict
217
- app.post('/api/governance/onboarding/case-review', (req, res) => {
225
+ app.post('/api/governance/onboarding/case-review', async (req, res) => {
218
226
  const user = auth(req, res);
219
227
  if (!user)
220
228
  return;
@@ -229,11 +237,11 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
229
237
  return void errorRes(res, 400, 'MISSING_REVIEWS', 'reviews 数组为空');
230
238
  }
231
239
  // 找用户的 pending_onboarding application
232
- const app_ = db.prepare(`
240
+ const app_ = await dbOne(`
233
241
  SELECT id, status FROM governance_applications
234
242
  WHERE user_id = ? AND role = ? AND status = 'pending_onboarding'
235
243
  ORDER BY created_at DESC LIMIT 1
236
- `).get(userId, role);
244
+ `, [userId, role]);
237
245
  if (!app_) {
238
246
  return void errorRes(res, 404, 'NO_PENDING_APPLICATION', `未找到 ${role} 待审申请,请先提交申请`);
239
247
  }
@@ -255,8 +263,10 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
255
263
  reasoning: r.reasoning.trim(),
256
264
  })),
257
265
  };
258
- db.prepare("UPDATE governance_applications SET case_review_text = ? WHERE id = ?")
259
- .run(JSON.stringify(payload), app_.id);
266
+ // Codex #234 P1:status 守卫,防 await 后申请被激活/解决时仍篡改 case_review audit 证据
267
+ const cu = await dbRun("UPDATE governance_applications SET case_review_text = ? WHERE id = ? AND status = 'pending_onboarding'", [JSON.stringify(payload), app_.id]);
268
+ if (cu.changes === 0)
269
+ return void errorRes(res, 409, 'APPLICATION_MOVED', '申请状态已变更(已被激活/解决),无法提交案例 review');
260
270
  res.json({
261
271
  success: true,
262
272
  application_id: app_.id,
@@ -267,11 +277,11 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
267
277
  // ─── Admin (maintainer activation flow, #1093 阶段 3) ─────────
268
278
  // spec docs/GOVERNANCE-ONBOARDING.md §4.4
269
279
  // GET /api/admin/governance/applications — 列出 pending_onboarding(可筛 quiz_passed + has_case_review)
270
- app.get('/api/admin/governance/applications', (req, res) => {
280
+ app.get('/api/admin/governance/applications', async (req, res) => {
271
281
  const admin = requireGovernanceAdmin(req, res);
272
282
  if (!admin)
273
283
  return;
274
- const items = db.prepare(`
284
+ const items = await dbAll(`
275
285
  SELECT ga.id, ga.user_id, ga.role, ga.action, ga.status, ga.quiz_score, ga.quiz_passed_at,
276
286
  CASE WHEN ga.case_review_text IS NOT NULL THEN 1 ELSE 0 END AS has_case_review,
277
287
  ga.created_at,
@@ -281,21 +291,21 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
281
291
  WHERE ga.status = 'pending_onboarding' AND ga.action = 'apply'
282
292
  ORDER BY ga.created_at ASC
283
293
  LIMIT 100
284
- `).all();
294
+ `);
285
295
  res.json({ items, count: items.length });
286
296
  });
287
297
  // GET /api/admin/governance/application/:id — 详情(含 expected_verdict 用于对比 — 仅 maintainer 看)
288
- app.get('/api/admin/governance/application/:id', (req, res) => {
298
+ app.get('/api/admin/governance/application/:id', async (req, res) => {
289
299
  const admin = requireGovernanceAdmin(req, res);
290
300
  if (!admin)
291
301
  return;
292
302
  const id = req.params.id;
293
- const row = db.prepare(`
303
+ const row = await dbOne(`
294
304
  SELECT ga.*, u.name AS user_name, u.handle, u.email
295
305
  FROM governance_applications ga
296
306
  JOIN users u ON u.id = ga.user_id
297
307
  WHERE ga.id = ?
298
- `).get(id);
308
+ `, [id]);
299
309
  if (!row)
300
310
  return void errorRes(res, 404, 'NOT_FOUND', 'application 不存在');
301
311
  const role = row.role;
@@ -319,7 +329,7 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
319
329
  // 4. INSERT action='activate' row + users.roles 加 role
320
330
  // 5. logAdminAction 留痕
321
331
  // body: { application_id, webauthn_token, note? }
322
- app.post('/api/admin/governance/activate', (req, res) => {
332
+ app.post('/api/admin/governance/activate', async (req, res) => {
323
333
  const admin = requireGovernanceAdmin(req, res);
324
334
  if (!admin)
325
335
  return;
@@ -332,10 +342,10 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
332
342
  return void errorRes(res, 400, 'MISSING_APPLICATION_ID', 'application_id 必填');
333
343
  }
334
344
  // 1. 找 pending application(predicate read,真 status check 在 transaction 内)
335
- const app_ = db.prepare(`
345
+ const app_ = await dbOne(`
336
346
  SELECT id, user_id, role, status, quiz_passed_at, case_review_text
337
347
  FROM governance_applications WHERE id = ?
338
- `).get(application_id);
348
+ `, [application_id]);
339
349
  if (!app_)
340
350
  return void errorRes(res, 404, 'NOT_FOUND', 'application 不存在');
341
351
  if (app_.status !== 'pending_onboarding') {
@@ -424,8 +434,10 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
424
434
  logAdminAction(adminId, 'governance_activate', 'user', app_.user_id, { role, application_id, note });
425
435
  // 7. 通知 user(站内)
426
436
  try {
427
- db.prepare(`INSERT INTO notifications (id, user_id, type, title, body, order_id) VALUES (?,?,?,?,?,?)`)
428
- .run(generateId('ntf'), app_.user_id, 'governance', `🎉 你的 ${role} 申请已通过`, `你已正式上岗 ${role}。本通知由 maintainer ${adminId} 签发。详 #me 治理面板。`, null);
437
+ await dbRun(`INSERT INTO notifications (id, user_id, type, title, body, order_id) VALUES (?,?,?,?,?,?)`, [generateId('ntf'), app_.user_id, 'governance',
438
+ `🎉 你的 ${role} 申请已通过`,
439
+ `你已正式上岗 ${role}。本通知由 maintainer ${adminId} 签发。详 #me 治理面板。`,
440
+ null]);
429
441
  }
430
442
  catch (_e) { /* notification 失败不阻塞 activate */ }
431
443
  res.json({
@@ -441,7 +453,7 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
441
453
  // POST /api/governance/onboarding/resign — 主动卸任
442
454
  // body: { role, confirm_text, webauthn_token }
443
455
  // confirm_text 必须等于 'RESIGN arbitrator' 或 'RESIGN verifier'(type-to-confirm 防误触)
444
- app.post('/api/governance/onboarding/resign', (req, res) => {
456
+ app.post('/api/governance/onboarding/resign', async (req, res) => {
445
457
  const user = auth(req, res);
446
458
  if (!user)
447
459
  return;
@@ -458,7 +470,7 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
458
470
  return void errorRes(res, 400, 'CONFIRM_MISMATCH', `请准确输入 "${expectedConfirm}" 确认卸任(type-to-confirm 防误触)`);
459
471
  }
460
472
  // 真源判定:users.roles JSON 包含该 role(防 1 user 多 active 行 / 防 active 行存量不一致)
461
- const userRow = db.prepare("SELECT roles FROM users WHERE id = ?").get(userId);
473
+ const userRow = await dbOne("SELECT roles FROM users WHERE id = ?", [userId]);
462
474
  let currentRoles = [];
463
475
  try {
464
476
  currentRoles = JSON.parse(userRow?.roles || '[]');
@@ -470,21 +482,21 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
470
482
  return void errorRes(res, 404, 'NOT_ACTIVE', `你当前不是 ${role},无需卸任`);
471
483
  }
472
484
  // 用于日志显示 + 后续 INSERT 关联(取最新的 active 行;若没有也容许,以 user.roles 为准)
473
- const activeRow = db.prepare(`
485
+ const activeRow = await dbOne(`
474
486
  SELECT id FROM governance_applications
475
487
  WHERE user_id = ? AND role = ? AND status = 'active'
476
488
  ORDER BY created_at DESC LIMIT 1
477
- `).get(userId, role);
489
+ `, [userId, role]);
478
490
  // active case 检查(spec §6.1:有未结案 → block,要求先 transfer/完成)
479
491
  // 只对 arbitrator 适用 — verifier 投票无长期 assigned
480
492
  if (role === 'arbitrator') {
481
493
  // disputes.assigned_arbitrators 是 JSON 数组;ruling_type IS NULL = 未结案
482
- const openCases = db.prepare(`
494
+ const openCases = await dbAll(`
483
495
  SELECT id FROM disputes
484
496
  WHERE ruling_type IS NULL
485
497
  AND assigned_arbitrators IS NOT NULL
486
498
  AND assigned_arbitrators LIKE ?
487
- `).all(`%"${userId}"%`);
499
+ `, [`%"${userId}"%`]);
488
500
  if (openCases.length > 0) {
489
501
  return void res.status(409).json({
490
502
  error: '尚有未结案 dispute',
@@ -563,7 +575,7 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
563
575
  // POST /api/governance/onboarding/appeal — auto_deactivate 后申诉
564
576
  // body: { source_application_id, appeal_reason }
565
577
  // 必须:source 行 action='auto_deactivate' + window 内 + 未已 appeal + reason 长度
566
- app.post('/api/governance/onboarding/appeal', (req, res) => {
578
+ app.post('/api/governance/onboarding/appeal', async (req, res) => {
567
579
  const user = auth(req, res);
568
580
  if (!user)
569
581
  return;
@@ -578,10 +590,10 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
578
590
  if (appeal_reason.length < minChars) {
579
591
  return void errorRes(res, 400, 'REASON_TOO_SHORT', `申诉理由至少 ${minChars} 字符,当前 ${appeal_reason.length}`);
580
592
  }
581
- const source = db.prepare(`
593
+ const source = await dbOne(`
582
594
  SELECT id, user_id, role, action, status, created_at
583
595
  FROM governance_applications WHERE id = ?
584
- `).get(source_application_id);
596
+ `, [source_application_id]);
585
597
  if (!source)
586
598
  return void errorRes(res, 404, 'SOURCE_NOT_FOUND', '原 application 不存在');
587
599
  if (source.user_id !== userId)
@@ -595,20 +607,20 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
595
607
  return void errorRes(res, 400, 'APPEAL_WINDOW_EXPIRED', `申诉窗口已过期(${windowDays} 天内可申诉)`);
596
608
  }
597
609
  // 已 appeal 过?(防重复)
598
- const existing = db.prepare(`
610
+ const existing = await dbOne(`
599
611
  SELECT id, status FROM governance_applications
600
612
  WHERE source_application_id = ? AND action = 'appeal'
601
613
  ORDER BY created_at DESC LIMIT 1
602
- `).get(source_application_id);
614
+ `, [source_application_id]);
603
615
  if (existing) {
604
616
  return void errorRes(res, 409, 'APPEAL_EXISTS', `已对该 application 提交过申诉(id=${existing.id} status=${existing.status})`);
605
617
  }
606
618
  const id = generateId('gapp');
607
- db.prepare(`
619
+ await dbRun(`
608
620
  INSERT INTO governance_applications
609
621
  (id, user_id, role, action, status, appeal_reason, source_application_id)
610
622
  VALUES (?, ?, ?, 'appeal', 'pending_review', ?, ?)
611
- `).run(id, userId, source.role, appeal_reason, source_application_id);
623
+ `, [id, userId, source.role, appeal_reason, source_application_id]);
612
624
  res.json({
613
625
  success: true,
614
626
  appeal_application_id: id,
@@ -618,12 +630,12 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
618
630
  });
619
631
  // GET /api/admin/governance/auto-deactivations — recent auto_deactivate audit
620
632
  // spec §6.2 公示触发原因(透明 — 元规则 #1)
621
- app.get('/api/admin/governance/auto-deactivations', (req, res) => {
633
+ app.get('/api/admin/governance/auto-deactivations', async (req, res) => {
622
634
  const admin = requireGovernanceAdmin(req, res);
623
635
  if (!admin)
624
636
  return;
625
637
  const limit = Math.min(200, Math.max(1, Number(req.query.limit) || 50));
626
- const items = db.prepare(`
638
+ const items = await dbAll(`
627
639
  SELECT ga.id, ga.user_id, ga.role, ga.appeal_reason AS trigger_reason,
628
640
  ga.cooldown_until, ga.created_at,
629
641
  u.name AS user_name, u.handle,
@@ -634,15 +646,15 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
634
646
  WHERE ga.action = 'auto_deactivate'
635
647
  ORDER BY ga.created_at DESC
636
648
  LIMIT ?
637
- `).all(limit);
649
+ `, [limit]);
638
650
  res.json({ items, count: items.length });
639
651
  });
640
652
  // GET /api/admin/governance/appeals — maintainer 看待裁决申诉
641
- app.get('/api/admin/governance/appeals', (req, res) => {
653
+ app.get('/api/admin/governance/appeals', async (req, res) => {
642
654
  const admin = requireGovernanceAdmin(req, res);
643
655
  if (!admin)
644
656
  return;
645
- const items = db.prepare(`
657
+ const items = await dbAll(`
646
658
  SELECT ga.id, ga.user_id, ga.role, ga.appeal_reason, ga.source_application_id, ga.created_at,
647
659
  u.name AS user_name, u.handle, u.email,
648
660
  src.created_at AS auto_deactivate_at
@@ -652,13 +664,13 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
652
664
  WHERE ga.action = 'appeal' AND ga.status = 'pending_review'
653
665
  ORDER BY ga.created_at ASC
654
666
  LIMIT 100
655
- `).all();
667
+ `);
656
668
  res.json({ items, count: items.length });
657
669
  });
658
670
  // POST /api/admin/governance/resolve-appeal — maintainer 裁决申诉
659
671
  // body: { appeal_application_id, decision: 'accept' | 'reject', resolution_text, webauthn_token }
660
672
  // accept → 恢复 active(spec §7.2) ;reject → 维持 inactive,公开理由
661
- app.post('/api/admin/governance/resolve-appeal', (req, res) => {
673
+ app.post('/api/admin/governance/resolve-appeal', async (req, res) => {
662
674
  const admin = requireGovernanceAdmin(req, res);
663
675
  if (!admin)
664
676
  return;
@@ -676,10 +688,10 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
676
688
  if (resolution_text.length < 30) {
677
689
  return void errorRes(res, 400, 'RESOLUTION_TOO_SHORT', '处置理由至少 30 字符(spec §7.2 公开理由)');
678
690
  }
679
- const appeal = db.prepare(`
691
+ const appeal = await dbOne(`
680
692
  SELECT id, user_id, role, action, status, source_application_id
681
693
  FROM governance_applications WHERE id = ?
682
- `).get(appeal_application_id);
694
+ `, [appeal_application_id]);
683
695
  if (!appeal)
684
696
  return void errorRes(res, 404, 'NOT_FOUND', 'appeal 不存在');
685
697
  if (appeal.action !== 'appeal')
@@ -748,8 +760,7 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
748
760
  const title = decision === 'accept'
749
761
  ? `✅ 你的 ${appeal.role} 申诉已通过`
750
762
  : `❌ 你的 ${appeal.role} 申诉被驳回`;
751
- db.prepare(`INSERT INTO notifications (id, user_id, type, title, body, order_id) VALUES (?,?,?,?,?,?)`)
752
- .run(generateId('ntf'), appeal.user_id, 'governance', title, resolution_text, null);
763
+ await dbRun(`INSERT INTO notifications (id, user_id, type, title, body, order_id) VALUES (?,?,?,?,?,?)`, [generateId('ntf'), appeal.user_id, 'governance', title, resolution_text, null]);
753
764
  }
754
765
  catch (_e) { /* ignore */ }
755
766
  res.json({
@@ -762,19 +773,19 @@ export function registerGovernanceOnboardingRoutes(app, deps) {
762
773
  });
763
774
  // GET /api/governance/onboarding/progress
764
775
  // 返回 onboarding 整体进度(spec §4):申请状态 + 学习包(client localStorage) + 题目分数 + 案例(后续)
765
- app.get('/api/governance/onboarding/progress', (req, res) => {
776
+ app.get('/api/governance/onboarding/progress', async (req, res) => {
766
777
  const user = auth(req, res);
767
778
  if (!user)
768
779
  return;
769
780
  const userId = user.id;
770
781
  // 各 role 最新 application
771
- const applications = db.prepare(`
782
+ const applications = await dbAll(`
772
783
  SELECT id, role, action, status, quiz_score, quiz_passed_at, case_review_text, cooldown_until, created_at
773
784
  FROM governance_applications
774
785
  WHERE user_id = ?
775
786
  ORDER BY created_at DESC
776
- `).all(userId);
777
- const param = db.prepare("SELECT value FROM protocol_params WHERE key = ?").get('governance_onboarding.quiz_pass_score');
787
+ `, [userId]);
788
+ const param = await dbOne("SELECT value FROM protocol_params WHERE key = ?", ['governance_onboarding.quiz_pass_score']);
778
789
  const passThreshold = param ? Number(param.value) : 80;
779
790
  res.json({
780
791
  applications,
@@ -1,3 +1,4 @@
1
+ import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
1
2
  const VALID_GB_DISCOUNT_MIN = 0.05;
2
3
  const VALID_GB_DISCOUNT_MAX = 0.50;
3
4
  /** 结算团购 — 成团创建订单 + 退差价;未达成全员退款。export 仅供 cron。 */
@@ -64,14 +65,14 @@ export function sweepExpiredGroupBuys(db, generateId, broadcastSystemEvent) {
64
65
  export function registerGroupBuysRoutes(app, deps) {
65
66
  const { db, generateId, auth, isTrustedRole, errorRes, broadcastSystemEvent } = deps;
66
67
  // 卖家开团
67
- app.post('/api/group-buys', (req, res) => {
68
+ app.post('/api/group-buys', async (req, res) => {
68
69
  const user = auth(req, res);
69
70
  if (!user)
70
71
  return;
71
72
  const { product_id, variant_id, target_count, discount_pct, duration_hours } = req.body || {};
72
73
  if (!product_id)
73
74
  return void res.status(400).json({ error: 'product_id 必填' });
74
- const p = db.prepare('SELECT id, seller_id, price, has_variants FROM products WHERE id = ? AND status = \'active\'').get(product_id);
75
+ const p = await dbOne('SELECT id, seller_id, price, has_variants FROM products WHERE id = ? AND status = \'active\'', [product_id]);
75
76
  if (!p)
76
77
  return void res.status(404).json({ error: '商品不存在或已下架' });
77
78
  if (p.seller_id !== user.id)
@@ -86,13 +87,12 @@ export function registerGroupBuysRoutes(app, deps) {
86
87
  if (Number(p.has_variants) === 1 && !variant_id)
87
88
  return void res.status(400).json({ error: '该商品有规格,请指定 variant_id' });
88
89
  if (variant_id) {
89
- const v = db.prepare('SELECT id FROM product_variants WHERE id = ? AND product_id = ? AND is_active = 1').get(variant_id, p.id);
90
+ const v = await dbOne('SELECT id FROM product_variants WHERE id = ? AND product_id = ? AND is_active = 1', [variant_id, p.id]);
90
91
  if (!v)
91
92
  return void res.status(400).json({ error: 'variant 不存在' });
92
93
  }
93
94
  const id = generateId('gb');
94
- db.prepare(`INSERT INTO group_buys (id, seller_id, product_id, variant_id, target_count, discount_pct, ends_at) VALUES (?,?,?,?,?,?,?)`)
95
- .run(id, user.id, p.id, variant_id || null, target, disc, endsAt);
95
+ await dbRun(`INSERT INTO group_buys (id, seller_id, product_id, variant_id, target_count, discount_pct, ends_at) VALUES (?,?,?,?,?,?,?)`, [id, user.id, p.id, variant_id || null, target, disc, endsAt]);
96
96
  try {
97
97
  broadcastSystemEvent('group_buy_created', '👥', `团购创建 ${id} · 目标 ${target} 人 · ${(disc * 100).toFixed(0)}% off`, p.id);
98
98
  }
@@ -100,8 +100,8 @@ export function registerGroupBuysRoutes(app, deps) {
100
100
  res.json({ success: true, id, ends_at: endsAt });
101
101
  });
102
102
  // 公开列表
103
- app.get('/api/group-buys/live', (_req, res) => {
104
- const rows = db.prepare(`
103
+ app.get('/api/group-buys/live', async (_req, res) => {
104
+ const rows = await dbAll(`
105
105
  SELECT gb.*, p.title as product_title, p.price as original_price, p.images, p.category,
106
106
  u.handle as seller_handle, u.name as seller_name,
107
107
  (SELECT COUNT(*) FROM group_buy_participants WHERE group_buy_id = gb.id AND status != 'refunded') as joined_count
@@ -110,31 +110,31 @@ export function registerGroupBuysRoutes(app, deps) {
110
110
  JOIN users u ON u.id = gb.seller_id
111
111
  WHERE gb.status = 'active' AND gb.ends_at > datetime('now')
112
112
  ORDER BY gb.ends_at ASC LIMIT 100
113
- `).all();
113
+ `, []);
114
114
  res.json({ items: rows });
115
115
  });
116
116
  // 详情 + participants
117
- app.get('/api/group-buys/:id', (req, res) => {
118
- const gb = db.prepare(`
117
+ app.get('/api/group-buys/:id', async (req, res) => {
118
+ const gb = await dbOne(`
119
119
  SELECT gb.*, p.title as product_title, p.price as original_price, p.images, p.category,
120
120
  u.handle as seller_handle, u.name as seller_name
121
121
  FROM group_buys gb
122
122
  JOIN products p ON p.id = gb.product_id
123
123
  JOIN users u ON u.id = gb.seller_id
124
124
  WHERE gb.id = ?
125
- `).get(req.params.id);
125
+ `, [req.params.id]);
126
126
  if (!gb)
127
127
  return void res.status(404).json({ error: '团购不存在' });
128
- const participants = db.prepare(`
128
+ const participants = await dbAll(`
129
129
  SELECT p.id, p.buyer_id, p.status, p.created_at, u.handle as buyer_handle
130
130
  FROM group_buy_participants p JOIN users u ON u.id = p.buyer_id
131
131
  WHERE p.group_buy_id = ? AND p.status != 'refunded'
132
132
  ORDER BY p.created_at ASC
133
- `).all(req.params.id);
133
+ `, [req.params.id]);
134
134
  res.json({ ...gb, participants });
135
135
  });
136
136
  // 加入团购
137
- app.post('/api/group-buys/:id/join', (req, res) => {
137
+ app.post('/api/group-buys/:id/join', async (req, res) => {
138
138
  const user = auth(req, res);
139
139
  if (!user)
140
140
  return;
@@ -143,7 +143,7 @@ export function registerGroupBuysRoutes(app, deps) {
143
143
  const { shipping_address } = req.body || {};
144
144
  if (!shipping_address)
145
145
  return void res.status(400).json({ error: '请填写收货地址' });
146
- const gb = db.prepare('SELECT id, seller_id, product_id, status, target_count, ends_at, discount_pct FROM group_buys WHERE id = ?').get(req.params.id);
146
+ const gb = await dbOne('SELECT id, seller_id, product_id, status, target_count, ends_at, discount_pct FROM group_buys WHERE id = ?', [req.params.id]);
147
147
  if (!gb)
148
148
  return void res.status(404).json({ error: '团购不存在' });
149
149
  if (gb.status !== 'active')
@@ -152,14 +152,14 @@ export function registerGroupBuysRoutes(app, deps) {
152
152
  return void res.status(400).json({ error: '团购已结束' });
153
153
  if (gb.seller_id === user.id)
154
154
  return void res.status(400).json({ error: '不可加入自己的团购' });
155
- const existing = db.prepare('SELECT id FROM group_buy_participants WHERE group_buy_id = ? AND buyer_id = ? AND status != \'refunded\'').get(gb.id, user.id);
155
+ const existing = await dbOne('SELECT id FROM group_buy_participants WHERE group_buy_id = ? AND buyer_id = ? AND status != \'refunded\'', [gb.id, user.id]);
156
156
  if (existing)
157
157
  return void res.status(400).json({ error: '已加入此团购' });
158
- const product = db.prepare('SELECT price FROM products WHERE id = ?').get(gb.product_id);
158
+ const product = await dbOne('SELECT price FROM products WHERE id = ?', [gb.product_id]);
159
159
  if (!product)
160
160
  return void res.status(500).json({ error: '商品记录缺失' });
161
161
  const escrow = Number(product.price);
162
- const wallet = db.prepare('SELECT balance FROM wallets WHERE user_id = ?').get(user.id);
162
+ const wallet = await dbOne('SELECT balance FROM wallets WHERE user_id = ?', [user.id]);
163
163
  if (!wallet || wallet.balance < escrow)
164
164
  return void res.status(400).json({ error: `余额不足:需 ${escrow} WAZ` });
165
165
  const id = generateId('gbp');
@@ -168,7 +168,7 @@ export function registerGroupBuysRoutes(app, deps) {
168
168
  .run(id, gb.id, user.id, String(shipping_address).slice(0, 200), escrow);
169
169
  db.prepare('UPDATE wallets SET balance = balance - ?, escrowed = escrowed + ? WHERE user_id = ?').run(escrow, escrow, user.id);
170
170
  })();
171
- const joined = db.prepare(`SELECT COUNT(*) as n FROM group_buy_participants WHERE group_buy_id = ? AND status != 'refunded'`).get(gb.id).n;
171
+ const joined = (await dbOne(`SELECT COUNT(*) as n FROM group_buy_participants WHERE group_buy_id = ? AND status != 'refunded'`, [gb.id])).n;
172
172
  if (joined >= gb.target_count) {
173
173
  try {
174
174
  settleGroupBuy(db, generateId, broadcastSystemEvent, gb.id);
@@ -184,13 +184,13 @@ export function registerGroupBuysRoutes(app, deps) {
184
184
  res.json({ success: true, id, joined_count: joined, target_count: gb.target_count });
185
185
  });
186
186
  // 离开团购
187
- app.delete('/api/group-buys/:id/leave', (req, res) => {
187
+ app.delete('/api/group-buys/:id/leave', async (req, res) => {
188
188
  const user = auth(req, res);
189
189
  if (!user)
190
190
  return;
191
- const p = db.prepare(`SELECT p.id, p.escrow_amount, p.status, gb.status as gb_status FROM group_buy_participants p
191
+ const p = await dbOne(`SELECT p.id, p.escrow_amount, p.status, gb.status as gb_status FROM group_buy_participants p
192
192
  JOIN group_buys gb ON gb.id = p.group_buy_id
193
- WHERE p.group_buy_id = ? AND p.buyer_id = ?`).get(req.params.id, user.id);
193
+ WHERE p.group_buy_id = ? AND p.buyer_id = ?`, [req.params.id, user.id]);
194
194
  if (!p)
195
195
  return void res.status(404).json({ error: '未加入此团购' });
196
196
  if (p.status === 'fulfilled')