@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,5 @@
1
1
  import { recordRatingReputation } from '../../layer4-economics/L4-3-reputation/reputation-engine.js';
2
+ import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
2
3
  const RATING_BLIND_DAYS = 14;
3
4
  function parseDim(v) {
4
5
  const n = Number(v);
@@ -7,20 +8,20 @@ function parseDim(v) {
7
8
  export function registerRatingsRoutes(app, deps) {
8
9
  const { db, generateId, auth, isTrustedRole, errorRes, broadcastSystemEvent } = deps;
9
10
  // buyer → seller 评价(一单一评,仅 completed 订单可评)
10
- app.post('/api/orders/:order_id/rating', (req, res) => {
11
+ app.post('/api/orders/:order_id/rating', async (req, res) => {
11
12
  const user = auth(req, res);
12
13
  if (!user)
13
14
  return;
14
15
  if (isTrustedRole(user))
15
16
  return void errorRes(res, 403, 'TRUSTED_ROLE_NO_TRADE', '受信角色无购物功能');
16
- const order = db.prepare('SELECT id, buyer_id, seller_id, product_id, status FROM orders WHERE id = ?').get(req.params.order_id);
17
+ const order = await dbOne('SELECT id, buyer_id, seller_id, product_id, status FROM orders WHERE id = ?', [req.params.order_id]);
17
18
  if (!order)
18
19
  return void res.status(404).json({ error: '订单不存在' });
19
20
  if (order.buyer_id !== user.id)
20
21
  return void res.status(403).json({ error: '仅买家可评价' });
21
22
  if (order.status !== 'completed')
22
23
  return void res.status(400).json({ error: '订单完成后才能评价' });
23
- const existing = db.prepare('SELECT order_id FROM order_ratings WHERE order_id = ?').get(order.id);
24
+ const existing = await dbOne('SELECT order_id FROM order_ratings WHERE order_id = ?', [order.id]);
24
25
  if (existing)
25
26
  return void res.status(400).json({ error: '已评价过,每单仅可评一次' });
26
27
  const stars = Number(req.body?.stars);
@@ -55,18 +56,18 @@ export function registerRatingsRoutes(app, deps) {
55
56
  res.json({ success: true });
56
57
  });
57
58
  // seller → buyer 反向评价
58
- app.post('/api/orders/:order_id/buyer-rating', (req, res) => {
59
+ app.post('/api/orders/:order_id/buyer-rating', async (req, res) => {
59
60
  const user = auth(req, res);
60
61
  if (!user)
61
62
  return;
62
- const order = db.prepare('SELECT id, buyer_id, seller_id, status FROM orders WHERE id = ?').get(req.params.order_id);
63
+ const order = await dbOne('SELECT id, buyer_id, seller_id, status FROM orders WHERE id = ?', [req.params.order_id]);
63
64
  if (!order)
64
65
  return void res.status(404).json({ error: '订单不存在' });
65
66
  if (order.seller_id !== user.id)
66
67
  return void res.status(403).json({ error: '仅卖家可评价买家' });
67
68
  if (order.status !== 'completed')
68
69
  return void res.status(400).json({ error: '订单完成后才能评价' });
69
- const existing = db.prepare('SELECT order_id FROM buyer_ratings WHERE order_id = ?').get(order.id);
70
+ const existing = await dbOne('SELECT order_id FROM buyer_ratings WHERE order_id = ?', [order.id]);
70
71
  if (existing)
71
72
  return void res.status(400).json({ error: '已评价过,每单仅可评一次' });
72
73
  const stars = Number(req.body?.stars);
@@ -96,21 +97,21 @@ export function registerRatingsRoutes(app, deps) {
96
97
  res.json({ success: true });
97
98
  });
98
99
  // 查 seller → buyer 评价(双盲遮蔽:buyer 看不到,除非自己也评过 OR 窗口到期)
99
- app.get('/api/orders/:order_id/buyer-rating', (req, res) => {
100
+ app.get('/api/orders/:order_id/buyer-rating', async (req, res) => {
100
101
  const user = auth(req, res);
101
102
  if (!user)
102
103
  return;
103
- const order = db.prepare('SELECT buyer_id, seller_id FROM orders WHERE id = ?').get(req.params.order_id);
104
+ const order = await dbOne('SELECT buyer_id, seller_id FROM orders WHERE id = ?', [req.params.order_id]);
104
105
  if (!order)
105
106
  return void res.status(404).json({ error: '订单不存在' });
106
107
  if (order.buyer_id !== user.id && order.seller_id !== user.id) {
107
108
  return void res.status(403).json({ error: '无权查看' });
108
109
  }
109
- const br = db.prepare(`SELECT stars, comment, dim_payment_speed, dim_communication, dim_responsiveness, hidden_until, created_at FROM buyer_ratings WHERE order_id = ?`).get(req.params.order_id);
110
+ const br = await dbOne(`SELECT stars, comment, dim_payment_speed, dim_communication, dim_responsiveness, hidden_until, created_at FROM buyer_ratings WHERE order_id = ?`, [req.params.order_id]);
110
111
  if (!br)
111
112
  return void res.json({ item: null });
112
113
  const isBuyerView = order.buyer_id === user.id;
113
- const buyerAlsoRated = !!db.prepare(`SELECT order_id FROM order_ratings WHERE order_id = ?`).get(req.params.order_id);
114
+ const buyerAlsoRated = !!(await dbOne(`SELECT order_id FROM order_ratings WHERE order_id = ?`, [req.params.order_id]));
114
115
  const blindExpired = br.hidden_until && new Date(br.hidden_until) < new Date();
115
116
  if (isBuyerView && !buyerAlsoRated && !blindExpired) {
116
117
  return void res.json({ item: { masked: true, hidden_until: br.hidden_until, reason: 'blind_until_both_or_expire' } });
@@ -118,50 +119,56 @@ export function registerRatingsRoutes(app, deps) {
118
119
  res.json({ item: br });
119
120
  });
120
121
  // 查 buyer → seller 评价(双盲遮蔽:seller 视角同样)
121
- app.get('/api/orders/:order_id/rating', (req, res) => {
122
+ app.get('/api/orders/:order_id/rating', async (req, res) => {
122
123
  const user = auth(req, res);
123
124
  if (!user)
124
125
  return;
125
- const order = db.prepare('SELECT buyer_id, seller_id FROM orders WHERE id = ?').get(req.params.order_id);
126
+ const order = await dbOne('SELECT buyer_id, seller_id FROM orders WHERE id = ?', [req.params.order_id]);
126
127
  if (!order)
127
128
  return void res.status(404).json({ error: '订单不存在' });
128
129
  if (order.buyer_id !== user.id && order.seller_id !== user.id) {
129
130
  return void res.status(403).json({ error: '无权查看' });
130
131
  }
131
- const r = db.prepare('SELECT stars, comment, reply, replied_at, buyer_followup, buyer_followup_at, dim_quality, dim_speed, dim_service, hidden_until, created_at FROM order_ratings WHERE order_id = ?').get(req.params.order_id);
132
+ const r = await dbOne('SELECT stars, comment, reply, replied_at, buyer_followup, buyer_followup_at, dim_quality, dim_speed, dim_service, hidden_until, created_at FROM order_ratings WHERE order_id = ?', [req.params.order_id]);
132
133
  if (!r)
133
134
  return void res.json({ item: null });
134
135
  const isSellerView = order.seller_id === user.id;
135
- const sellerAlsoRated = !!db.prepare(`SELECT order_id FROM buyer_ratings WHERE order_id = ?`).get(req.params.order_id);
136
+ const sellerAlsoRated = !!(await dbOne(`SELECT order_id FROM buyer_ratings WHERE order_id = ?`, [req.params.order_id]));
136
137
  const blindExpired = r.hidden_until && new Date(r.hidden_until) < new Date();
137
138
  if (isSellerView && !sellerAlsoRated && !blindExpired) {
138
139
  return void res.json({ item: { masked: true, hidden_until: r.hidden_until, reason: 'blind_until_both_or_expire' } });
139
140
  }
140
141
  res.json({ item: r });
141
142
  });
142
- app.post('/api/orders/:order_id/rating/reply', (req, res) => {
143
+ app.post('/api/orders/:order_id/rating/reply', async (req, res) => {
143
144
  const user = auth(req, res);
144
145
  if (!user)
145
146
  return;
146
- const r = db.prepare('SELECT seller_id, reply FROM order_ratings WHERE order_id = ?').get(req.params.order_id);
147
+ const r = await dbOne('SELECT seller_id, reply, hidden_until FROM order_ratings WHERE order_id = ?', [req.params.order_id]);
147
148
  if (!r)
148
149
  return void res.status(404).json({ error: '该订单暂无评价' });
149
150
  if (r.seller_id !== user.id)
150
151
  return void res.status(403).json({ error: '仅卖家可回复' });
152
+ // 双盲铁律:未揭晓前不能回复(回复=已读到评价)。揭晓条件 = 自己也评过买家 OR 盲评期已过。
153
+ const sellerAlsoRated = !!(await dbOne(`SELECT order_id FROM buyer_ratings WHERE order_id = ?`, [req.params.order_id]));
154
+ const blindExpired = !!r.hidden_until && new Date(r.hidden_until) < new Date();
155
+ if (!sellerAlsoRated && !blindExpired) {
156
+ return void res.status(403).json({ error: '双盲期未结束:请先评价买家,或等盲评期满后再回应', error_code: 'RATING_STILL_BLIND' });
157
+ }
151
158
  if (r.reply)
152
159
  return void res.status(400).json({ error: '已回复过,每条评价仅可回复一次' });
153
160
  const reply = req.body?.reply ? String(req.body.reply).slice(0, 500) : null;
154
161
  if (!reply)
155
162
  return void res.status(400).json({ error: '回复不能为空' });
156
- db.prepare(`UPDATE order_ratings SET reply = ?, replied_at = datetime('now') WHERE order_id = ?`).run(reply, req.params.order_id);
163
+ await dbRun(`UPDATE order_ratings SET reply = ?, replied_at = datetime('now') WHERE order_id = ?`, [reply, req.params.order_id]);
157
164
  res.json({ success: true });
158
165
  });
159
166
  // W3 买家追问 — 在卖家 reply 后可追问一次
160
- app.post('/api/orders/:order_id/rating/followup', (req, res) => {
167
+ app.post('/api/orders/:order_id/rating/followup', async (req, res) => {
161
168
  const user = auth(req, res);
162
169
  if (!user)
163
170
  return;
164
- const r = db.prepare('SELECT buyer_id, reply, buyer_followup FROM order_ratings WHERE order_id = ?').get(req.params.order_id);
171
+ const r = await dbOne('SELECT buyer_id, reply, buyer_followup FROM order_ratings WHERE order_id = ?', [req.params.order_id]);
165
172
  if (!r)
166
173
  return void res.status(404).json({ error: '该订单暂无评价' });
167
174
  if (r.buyer_id !== user.id)
@@ -173,15 +180,14 @@ export function registerRatingsRoutes(app, deps) {
173
180
  const followup = req.body?.followup ? String(req.body.followup).trim().slice(0, 200) : '';
174
181
  if (followup.length < 2)
175
182
  return void res.status(400).json({ error: '追问内容至少 2 字' });
176
- db.prepare(`UPDATE order_ratings SET buyer_followup = ?, buyer_followup_at = datetime('now') WHERE order_id = ?`)
177
- .run(followup, req.params.order_id);
183
+ await dbRun(`UPDATE order_ratings SET buyer_followup = ?, buyer_followup_at = datetime('now') WHERE order_id = ?`, [followup, req.params.order_id]);
178
184
  res.json({ success: true });
179
185
  });
180
186
  // 公开:商品评价 + 聚合(仅展示双盲已揭晓的)
181
- app.get('/api/products/:product_id/ratings', (req, res) => {
187
+ app.get('/api/products/:product_id/ratings', async (req, res) => {
182
188
  const limit = Math.min(50, Math.max(1, Number(req.query.limit) || 20));
183
189
  const blindOpen = `(EXISTS (SELECT 1 FROM buyer_ratings br WHERE br.order_id = r.order_id) OR r.hidden_until IS NULL OR datetime(r.hidden_until) <= datetime('now'))`;
184
- const rows = db.prepare(`
190
+ const rows = await dbAll(`
185
191
  SELECT r.stars, r.comment, r.reply, r.replied_at, r.buyer_followup, r.buyer_followup_at, r.created_at,
186
192
  r.dim_quality, r.dim_speed, r.dim_service,
187
193
  u.name as buyer_name, u.handle as buyer_handle
@@ -189,8 +195,8 @@ export function registerRatingsRoutes(app, deps) {
189
195
  JOIN users u ON u.id = r.buyer_id
190
196
  WHERE r.product_id = ? AND ${blindOpen}
191
197
  ORDER BY r.created_at DESC LIMIT ?
192
- `).all(req.params.product_id, limit);
193
- const agg = db.prepare(`
198
+ `, [req.params.product_id, limit]);
199
+ const agg = await dbOne(`
194
200
  SELECT COUNT(*) as cnt, COALESCE(AVG(stars), 0) as avg_stars,
195
201
  SUM(CASE WHEN stars = 5 THEN 1 ELSE 0 END) as s5,
196
202
  SUM(CASE WHEN stars = 4 THEN 1 ELSE 0 END) as s4,
@@ -198,23 +204,77 @@ export function registerRatingsRoutes(app, deps) {
198
204
  SUM(CASE WHEN stars = 2 THEN 1 ELSE 0 END) as s2,
199
205
  SUM(CASE WHEN stars = 1 THEN 1 ELSE 0 END) as s1
200
206
  FROM order_ratings r WHERE product_id = ? AND ${blindOpen}
201
- `).get(req.params.product_id);
207
+ `, [req.params.product_id]);
202
208
  res.json({ items: rows, agg });
203
209
  });
204
- // 公开:卖家评价聚合(卖家主页)
205
- app.get('/api/sellers/:seller_id/ratings', (req, res) => {
210
+ // 卖家:自己店铺收到的全部评价(含 order_id 便于逐条回复 + 回复/追问状态)。
211
+ // 与公开聚合 endpoint 分开:authed + 只返回本人的评价 + 暴露 order_id(仅给卖家本人)。
212
+ // 纯只读,不改任何评价 / 资金逻辑;回复仍走既有 POST /orders/:order_id/rating/reply。
213
+ // ⚠️ 必须注册在 /api/sellers/:seller_id/ratings 【之前】,否则 'me' 会被 :seller_id 参数路由抢匹配。
214
+ app.get('/api/sellers/me/ratings', async (req, res) => {
215
+ const user = auth(req, res);
216
+ if (!user)
217
+ return;
218
+ const limit = Math.min(100, Math.max(1, Number(req.query.limit) || 50));
219
+ // 双盲铁律:卖家看 buyer→seller 评价,必须【自己也评过买家】(buyer_ratings 存在) 或【盲评期已过】(hidden_until 到期)。
220
+ // 否则只返回遮蔽行(不含 stars/comment/reply),与 GET /orders/:id/rating 的揭晓条件一致 —— 防卖家看了买家评价再反向报复。
221
+ const rows = await dbAll(`
222
+ SELECT r.order_id, r.stars, r.comment, r.reply, r.replied_at, r.buyer_followup, r.buyer_followup_at, r.created_at, r.product_id, r.hidden_until,
223
+ p.title as product_title,
224
+ u.name as buyer_name, u.handle as buyer_handle,
225
+ (SELECT 1 FROM buyer_ratings br WHERE br.order_id = r.order_id) AS seller_also_rated
226
+ FROM order_ratings r
227
+ JOIN products p ON p.id = r.product_id
228
+ JOIN users u ON u.id = r.buyer_id
229
+ WHERE r.seller_id = ?
230
+ ORDER BY r.created_at DESC LIMIT ?
231
+ `, [user.id, limit]);
232
+ const now = Date.now();
233
+ let unreplied = 0;
234
+ const items = rows.map(r => {
235
+ const blindExpired = !!r.hidden_until && new Date(r.hidden_until).getTime() < now;
236
+ const revealed = !!r.seller_also_rated || blindExpired;
237
+ if (!revealed) {
238
+ // 遮蔽:只回最小信息(有评价 + 商品 + 解除条件),绝不泄露分数/评论/回复
239
+ return { order_id: r.order_id, product_title: r.product_title, created_at: r.created_at, hidden_until: r.hidden_until, masked: true, reveal_reason: 'blind_until_both_or_expire' };
240
+ }
241
+ if (!r.reply)
242
+ unreplied++;
243
+ return {
244
+ order_id: r.order_id, stars: r.stars, comment: r.comment, reply: r.reply, replied_at: r.replied_at,
245
+ buyer_followup: r.buyer_followup, buyer_followup_at: r.buyer_followup_at, created_at: r.created_at,
246
+ product_id: r.product_id, product_title: r.product_title, buyer_name: r.buyer_name, buyer_handle: r.buyer_handle,
247
+ masked: false,
248
+ };
249
+ });
250
+ // 聚合双盲铁律:cnt / avg_stars 必须【只算已揭晓评价】,否则盲评期内卖家能从均分反推买家未揭晓评分。
251
+ // 与公开面同 blindOpen 条件;另回 masked_count(只告知"有多少条遮蔽中",不含分数)。
252
+ const blindOpen = `(EXISTS (SELECT 1 FROM buyer_ratings br WHERE br.order_id = r.order_id) OR r.hidden_until IS NULL OR datetime(r.hidden_until) <= datetime('now'))`;
253
+ const agg = await dbOne(`
254
+ SELECT
255
+ SUM(CASE WHEN ${blindOpen} THEN 1 ELSE 0 END) as cnt,
256
+ COALESCE(AVG(CASE WHEN ${blindOpen} THEN stars END), 0) as avg_stars,
257
+ SUM(CASE WHEN ${blindOpen} THEN 0 ELSE 1 END) as masked_count
258
+ FROM order_ratings r WHERE r.seller_id = ?`, [user.id]);
259
+ res.json({ items, agg: { ...(agg || {}), unreplied } });
260
+ });
261
+ // 公开:卖家评价聚合(卖家主页)。注册在 /me 之后(见上面注释)。
262
+ app.get('/api/sellers/:seller_id/ratings', async (req, res) => {
206
263
  const limit = Math.min(50, Math.max(1, Number(req.query.limit) || 20));
207
- const rows = db.prepare(`
264
+ // 双盲铁律(公开面):只展示已揭晓的评价 —— 与 GET /products/:id/ratings 同条件。
265
+ // 揭晓 = 双方都评过(buyer_ratings 存在) OR 无盲评窗口(hidden_until 空) OR 盲评期已过。
266
+ const blindOpen = `(EXISTS (SELECT 1 FROM buyer_ratings br WHERE br.order_id = r.order_id) OR r.hidden_until IS NULL OR datetime(r.hidden_until) <= datetime('now'))`;
267
+ const rows = await dbAll(`
208
268
  SELECT r.stars, r.comment, r.reply, r.replied_at, r.buyer_followup, r.buyer_followup_at, r.created_at, r.product_id,
209
269
  p.title as product_title,
210
270
  u.name as buyer_name, u.handle as buyer_handle
211
271
  FROM order_ratings r
212
272
  JOIN products p ON p.id = r.product_id
213
273
  JOIN users u ON u.id = r.buyer_id
214
- WHERE r.seller_id = ?
274
+ WHERE r.seller_id = ? AND ${blindOpen}
215
275
  ORDER BY r.created_at DESC LIMIT ?
216
- `).all(req.params.seller_id, limit);
217
- const agg = db.prepare(`SELECT COUNT(*) as cnt, COALESCE(AVG(stars), 0) as avg_stars FROM order_ratings WHERE seller_id = ?`).get(req.params.seller_id);
276
+ `, [req.params.seller_id, limit]);
277
+ const agg = await dbOne(`SELECT COUNT(*) as cnt, COALESCE(AVG(stars), 0) as avg_stars FROM order_ratings r WHERE r.seller_id = ? AND ${blindOpen}`, [req.params.seller_id]);
218
278
  res.json({ items: rows, agg });
219
279
  });
220
280
  }
@@ -1,8 +1,18 @@
1
+ import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
1
2
  export function registerRecoverKeyRoutes(app, deps) {
2
- const { db, internalAuditorId, issueCode, findActiveCode, CODE_TTL_MIN, MAX_CODE_ATTEMPTS } = deps;
3
+ // db 已走 RFC-016 异步 seam(dbOne/dbAll/dbRun),不再直接用 deps.db
4
+ const { internalAuditorId, issueCode, findActiveCode, canDeliverCodes, emailDeliveryNotConfigured, hashPassword, CODE_TTL_MIN, MAX_CODE_ATTEMPTS } = deps;
5
+ // 账号标识解析 —— 与 /api/login 一致:@handle / handle(小写)优先,name 兜底。
6
+ // 找回三步全部复用它,否则用 handle 登录的用户(如 @holden)在找回页按 name 永远查不到、邮件不发。
7
+ const accountRef = (raw) => {
8
+ const display = String(raw || '').trim();
9
+ return { display, handleRef: display.replace(/^@/, '').toLowerCase() };
10
+ };
11
+ // (handle = ? OR name = ?) 子句 + 参数,排除 sys/auditor。
12
+ const ACCOUNT_MATCH = "(lower(coalesce(handle, '')) = ? OR name = ?) AND id NOT IN ('sys_protocol', ?)";
3
13
  // IP 级速率(5/min)— 防爆破列举账户
4
14
  const recoverKeyHits = new Map();
5
- app.post('/api/recover-key', (req, res) => {
15
+ app.post('/api/recover-key', async (req, res) => {
6
16
  const ip = req.ip || '';
7
17
  if (ip) {
8
18
  const now = Date.now();
@@ -23,10 +33,11 @@ export function registerRecoverKeyRoutes(app, deps) {
23
33
  }
24
34
  const { name } = req.body;
25
35
  if (!name?.trim())
26
- return void res.json({ error: '请填写注册时使用的名称' });
27
- const rows = db.prepare("SELECT name, role, api_key, email, phone, created_at FROM users WHERE name = ? AND id NOT IN ('sys_protocol', ?)").all(name.trim(), internalAuditorId);
36
+ return void res.json({ error: '请填写注册时使用的名称或 @用户名' });
37
+ const { display, handleRef } = accountRef(name);
38
+ const rows = await dbAll(`SELECT name, role, api_key, email, phone, created_at FROM users WHERE ${ACCOUNT_MATCH}`, [handleRef, display, internalAuditorId]);
28
39
  if (rows.length === 0)
29
- return void res.json({ error: '未找到该名称的账号' });
40
+ return void res.json({ error: '未找到该名称 / @用户名的账号' });
30
41
  const mask = (s) => s && s.length > 8 ? `${s.slice(0, 4)}…${s.slice(-4)}` : s;
31
42
  const maskEmail = (e) => {
32
43
  if (!e)
@@ -54,47 +65,76 @@ export function registerRecoverKeyRoutes(app, deps) {
54
65
  });
55
66
  });
56
67
  // 步骤 1:发送验证码到已绑定邮箱(防泄露:找没找到都同响应)
57
- app.post('/api/recover-key/start', (req, res) => {
68
+ app.post('/api/recover-key/start', async (req, res) => {
58
69
  const { name, email } = req.body;
59
70
  if (!name?.trim() || !email?.trim())
60
71
  return void res.json({ error: '请填写名称和邮箱' });
72
+ if (!canDeliverCodes()) {
73
+ const unavailable = emailDeliveryNotConfigured();
74
+ return void res.status(unavailable.status).json({ error: unavailable.error, error_code: unavailable.error_code });
75
+ }
61
76
  const target = email.trim().toLowerCase();
62
- const user = db.prepare(`
63
- SELECT id, name, email FROM users
64
- WHERE name = ? AND email = ? AND email_verified = 1
65
- AND id NOT IN ('sys_protocol', ?) LIMIT 1
66
- `).get(name.trim(), target, internalAuditorId);
67
- if (user)
68
- issueCode(user.id, 'email', target, 'recover_key');
69
- res.json({
77
+ const genericResponse = {
70
78
  success: true,
71
79
  notice: '若该名称与邮箱组合存在,验证码已发送至该邮箱',
72
80
  expires_in_min: CODE_TTL_MIN,
73
- });
81
+ };
82
+ const { display, handleRef } = accountRef(name);
83
+ const user = await dbOne(`
84
+ SELECT id, name, email FROM users
85
+ WHERE ${ACCOUNT_MATCH} AND email = ? AND email_verified = 1 LIMIT 1
86
+ `, [handleRef, display, internalAuditorId, target]);
87
+ if (user) {
88
+ const issued = await issueCode(user.id, 'email', target, 'recover_key');
89
+ if (!issued.ok) {
90
+ console.warn(`[recover-key] verification email delivery failed: ${issued.error_code}`);
91
+ return void res.json(genericResponse);
92
+ }
93
+ }
94
+ res.json(genericResponse);
74
95
  });
75
- // 步骤 2:提交验证码 → 返回完整 api_key
76
- app.post('/api/recover-key/confirm', (req, res) => {
77
- const { name, email, code } = req.body;
96
+ // 步骤 2:提交验证码 → 返回完整 api_key,并可选同时重置登录密码(code + new_password)。
97
+ // 安全等价:本端点本就返回完整 api_key(最高凭证),允许同时重置密码不扩大权限面 —— 验证码已是同等门槛。
98
+ app.post('/api/recover-key/confirm', async (req, res) => {
99
+ const { name, email, code, new_password } = req.body;
78
100
  if (!name?.trim() || !email?.trim() || !code?.trim())
79
101
  return void res.json({ error: '请填写完整信息' });
102
+ // 可选新密码:格式与 change-password 一致(≥8,≤200)。先校验格式,失败【不消费验证码】,可重试。
103
+ const wantsPasswordReset = new_password !== undefined && new_password !== null && String(new_password) !== '';
104
+ if (wantsPasswordReset) {
105
+ if (String(new_password).length < 8)
106
+ return void res.json({ error: '新密码至少 8 字符' });
107
+ if (String(new_password).length > 200)
108
+ return void res.json({ error: '密码过长(>200 字符)' });
109
+ }
80
110
  const target = email.trim().toLowerCase();
81
111
  const row = findActiveCode('email', target, 'recover_key');
82
112
  if (!row)
83
113
  return void res.json({ error: '验证码已过期或未发送,请重新开始' });
84
- const user = db.prepare(`SELECT id, name, api_key FROM users WHERE id = ?`).get(row.user_id);
85
- if (!user || user.name !== name.trim())
86
- return void res.json({ error: '名称与验证码不匹配' });
114
+ const user = await dbOne(`SELECT id, name, handle, api_key FROM users WHERE id = ?`, [row.user_id]);
115
+ const { display, handleRef } = accountRef(name);
116
+ const refMatches = !!user && (user.name === display || String(user.handle || '').toLowerCase() === handleRef);
117
+ if (!user || !refMatches)
118
+ return void res.json({ error: '名称 / @用户名与验证码不匹配' });
87
119
  if (String(row.code) !== code.trim()) {
88
120
  const attempts = row.attempts + 1;
89
121
  if (attempts >= MAX_CODE_ATTEMPTS) {
90
- db.prepare("UPDATE verification_codes SET attempts = ?, used_at = datetime('now') WHERE id = ?")
91
- .run(attempts, row.id);
122
+ await dbRun("UPDATE verification_codes SET attempts = ?, used_at = datetime('now') WHERE id = ?", [attempts, row.id]);
92
123
  return void res.json({ error: '错误次数过多,验证码已作废,请重新开始' });
93
124
  }
94
- db.prepare("UPDATE verification_codes SET attempts = ? WHERE id = ?").run(attempts, row.id);
125
+ await dbRun("UPDATE verification_codes SET attempts = ? WHERE id = ?", [attempts, row.id]);
95
126
  return void res.json({ error: `验证码错误(剩余 ${MAX_CODE_ATTEMPTS - attempts} 次)` });
96
127
  }
97
- db.prepare("UPDATE verification_codes SET used_at = datetime('now') WHERE id = ?").run(row.id);
98
- res.json({ success: true, api_key: user.api_key, name: user.name });
128
+ await dbRun("UPDATE verification_codes SET used_at = datetime('now') WHERE id = ?", [row.id]);
129
+ // optional password reset — same credential gate as returning the api_key, so no extra power.
130
+ let passwordReset = false;
131
+ if (wantsPasswordReset) {
132
+ // mirror /profile/set-password: also clear lock state, else a user who forgot + got locked out by
133
+ // failed attempts stays locked and "new password is correct but can't log in" (auth-login rejects
134
+ // locked users before verifying the password).
135
+ await dbRun("UPDATE users SET password_hash = ?, failed_attempts = 0, locked_until = NULL, updated_at = datetime('now') WHERE id = ?", [hashPassword(String(new_password)), user.id]);
136
+ passwordReset = true;
137
+ }
138
+ res.json({ success: true, api_key: user.api_key, name: user.name, ...(passwordReset ? { password_reset: true } : {}) });
99
139
  });
100
140
  }
@@ -1,28 +1,31 @@
1
+ import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
1
2
  export function registerReferralRoutes(app, deps) {
2
- const { db, auth, requireProtocolAdmin, logAdminAction, issueInviteSlot, inviteRotationLookup } = deps;
3
+ // db 已全量走 RFC-016 异步 seam(dbOne/dbAll/dbRun),不再直接用 deps.db
4
+ const { auth, requireProtocolAdmin, logAdminAction, issueInviteSlot, inviteRotationLookup } = deps;
3
5
  // B-1: 个人邀请 dashboard
4
- app.get('/api/referral/me', (req, res) => {
6
+ app.get('/api/referral/me', async (req, res) => {
5
7
  const user = auth(req, res);
6
8
  if (!user)
7
9
  return;
8
10
  const code = user.permanent_code || null;
9
11
  // 我直接邀请的人
10
- const directInvitees = db.prepare(`
12
+ const directInvitees = await dbAll(`
11
13
  SELECT u.id, u.handle, u.name, u.role, u.created_at,
12
14
  (SELECT COUNT(*) FROM orders WHERE buyer_id = u.id AND status = 'completed') as completed_orders,
13
15
  (SELECT COALESCE(SUM(total_amount), 0) FROM orders WHERE buyer_id = u.id AND status = 'completed') as gmv
14
16
  FROM users u WHERE u.sponsor_id = ?
15
17
  ORDER BY u.created_at DESC LIMIT 50
16
- `).all(user.id);
18
+ `, [user.id]);
17
19
  // 推土机奖励 / 商品分享佣金(commission_records 按订单粒度)
18
- const earnings = db.prepare(`
20
+ const earnings = (await dbOne(`
19
21
  SELECT COUNT(*) as cnt, COALESCE(SUM(amount), 0) as total FROM commission_records WHERE beneficiary_id = ?
20
- `).get(user.id);
21
- const todayEarnings = db.prepare(`SELECT COALESCE(SUM(amount), 0) as t FROM commission_records WHERE beneficiary_id = ? AND created_at > datetime('now', '-1 day')`).get(user.id).t;
22
- const monthEarnings = db.prepare(`SELECT COALESCE(SUM(amount), 0) as t FROM commission_records WHERE beneficiary_id = ? AND created_at > datetime('now', '-30 days')`).get(user.id).t;
22
+ `, [user.id]));
23
+ const todayEarnings = (await dbOne(`SELECT COALESCE(SUM(amount), 0) as t FROM commission_records WHERE beneficiary_id = ? AND created_at > datetime('now', '-1 day')`, [user.id])).t;
24
+ const monthEarnings = (await dbOne(`SELECT COALESCE(SUM(amount), 0) as t FROM commission_records WHERE beneficiary_id = ? AND created_at > datetime('now', '-30 days')`, [user.id])).t;
23
25
  res.json({
24
26
  invite_code: code,
25
- invite_link: code ? `${req.protocol}://${req.get('host')}/?ref=${code}` : null,
27
+ invite_link: code ? `${req.protocol}://${req.get('host')}/i/${code}` : null,
28
+ invite_unavailable_reason: code ? null : 'permanent_code_missing — refresh or contact support',
26
29
  direct_invitees_count: directInvitees.length,
27
30
  direct_invitees: directInvitees,
28
31
  earnings: {
@@ -34,8 +37,8 @@ export function registerReferralRoutes(app, deps) {
34
37
  });
35
38
  });
36
39
  // 公开邀请码轮询(开关 ON 时)
37
- app.post('/api/invite/rotate', (_req, res) => {
38
- const enabled = db.prepare("SELECT value FROM system_state WHERE key='invite_rotation_enabled'").get()?.value === '1';
40
+ app.post('/api/invite/rotate', async (_req, res) => {
41
+ const enabled = (await dbOne("SELECT value FROM system_state WHERE key='invite_rotation_enabled'"))?.value === '1';
39
42
  if (!enabled)
40
43
  return void res.status(403).json({ error: '邀请码获取暂未开放', enabled: false });
41
44
  const slot = issueInviteSlot();
@@ -45,38 +48,37 @@ export function registerReferralRoutes(app, deps) {
45
48
  res.json({ enabled: true, code: u.code });
46
49
  });
47
50
  // protocol 开关
48
- app.post('/api/admin/invite-rotation/toggle', (req, res) => {
51
+ app.post('/api/admin/invite-rotation/toggle', async (req, res) => {
49
52
  const admin = requireProtocolAdmin(req, res);
50
53
  if (!admin)
51
54
  return;
52
55
  const { enabled } = req.body;
53
56
  const v = enabled ? '1' : '0';
54
- db.prepare("INSERT OR REPLACE INTO system_state (key, value) VALUES ('invite_rotation_enabled', ?)").run(v);
57
+ await dbRun("INSERT OR REPLACE INTO system_state (key, value) VALUES ('invite_rotation_enabled', ?)", [v]);
55
58
  logAdminAction(admin.id, 'invite_rotation_toggle', 'system', 'invite_rotation_enabled', { value: v });
56
59
  res.json({ success: true, enabled: !!enabled });
57
60
  });
58
61
  // RFC-003 #1122: 生成商品分享链接(把 MCP webaz_share_link 的本地计算搬到服务端,
59
62
  // 让 MCP NETWORK 模式可代理)。RFC-002 §3.5 valuation-layer gate:需 rewards opt-in。
60
- // PWA pickPreferredSide 对齐的 side 选择(team_count | pv_count)。
61
- app.get('/api/share-link', (req, res) => {
63
+ // pre-public 去左右码:不再接受/返回 side,放置侧别由注册时系统自动决定。
64
+ app.get('/api/share-link', async (req, res) => {
62
65
  const user = auth(req, res);
63
66
  if (!user)
64
67
  return;
65
68
  const userId = user.id;
66
69
  const productId = String(req.query.product_id || '');
67
- const sideArg = String(req.query.side || 'auto');
68
70
  if (!productId)
69
71
  return void res.status(400).json({ error: 'product_id required', error_code: 'PRODUCT_ID_REQUIRED' });
70
- const optIn = db.prepare("SELECT rewards_opted_in FROM users WHERE id = ?").get(userId)?.rewards_opted_in ?? 0;
72
+ const optIn = (await dbOne("SELECT rewards_opted_in FROM users WHERE id = ?", [userId]))?.rewards_opted_in ?? 0;
71
73
  if (optIn !== 1) {
72
- const getParam = (key, def) => {
73
- const r = db.prepare("SELECT value FROM protocol_params WHERE key = ?").get(key);
74
+ const getParam = async (key, def) => {
75
+ const r = await dbOne("SELECT value FROM protocol_params WHERE key = ?", [key]);
74
76
  return r ? Number(r.value) : def;
75
77
  };
76
- const minOrders = getParam('rewards_opt_in.min_completed_orders', 1);
77
- const requirePasskey = getParam('rewards_opt_in.require_passkey', 1);
78
- const totalCompleted = db.prepare("SELECT COUNT(*) as n FROM orders WHERE buyer_id = ? AND status = 'completed'").get(userId).n;
79
- const passkeyCount = db.prepare("SELECT COUNT(*) as n FROM webauthn_credentials WHERE user_id = ?").get(userId).n;
78
+ const minOrders = await getParam('rewards_opt_in.min_completed_orders', 1);
79
+ const requirePasskey = await getParam('rewards_opt_in.require_passkey', 1);
80
+ const totalCompleted = (await dbOne("SELECT COUNT(*) as n FROM orders WHERE buyer_id = ? AND status = 'completed'", [userId])).n;
81
+ const passkeyCount = (await dbOne("SELECT COUNT(*) as n FROM webauthn_credentials WHERE user_id = ?", [userId])).n;
80
82
  const missing = [];
81
83
  if (totalCompleted < minOrders)
82
84
  missing.push(`completed_orders ${totalCompleted}/${minOrders}`);
@@ -86,50 +88,33 @@ export function registerReferralRoutes(app, deps) {
86
88
  missing.push('application_not_submitted');
87
89
  return void res.status(403).json({
88
90
  error: 'rewards_opt_in_required',
89
- message: 'Share-link generation is a valuation-layer action — requires builder-identity opt-in (RFC-002 §3.5)',
91
+ message: 'Share-link generation is a valuation-layer (rewards / share-link) action, NOT a contribution gate — requires rewards / share-commission opt-in (RFC-002 §3.5)',
90
92
  missing_requirements: missing,
91
93
  next_steps: [
92
- 'Open PWA #me → tap "申请共建身份 / Apply for builder identity"',
94
+ 'Open PWA #me → tap "申请分享分润 / Enable share-commission opt-in"',
93
95
  'Read the 8-second disclosure (cannot skip)',
94
96
  'Submit application — pre-checks run server-side',
95
97
  ],
96
98
  });
97
99
  }
98
- const product = db.prepare("SELECT id, title, price, commission_rate FROM products WHERE id = ? AND status='active'").get(productId);
100
+ const product = await dbOne("SELECT id, title, price, commission_rate FROM products WHERE id = ? AND status='active'", [productId]);
99
101
  if (!product)
100
102
  return void res.status(404).json({ error: '商品不存在或已下架', error_code: 'PRODUCT_NOT_FOUND' });
101
- let side = 'right';
102
- if (sideArg === 'left' || sideArg === 'right') {
103
- side = sideArg;
104
- }
105
- else {
106
- const u = db.prepare("SELECT placement_pref, total_left_pv, total_right_pv, left_count, right_count FROM users WHERE id = ?")
107
- .get(userId);
108
- const pref = u?.placement_pref || 'team_count';
109
- if (pref === 'pv_count') {
110
- const since = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000).toISOString().slice(0, 19).replace('T', ' ');
111
- const w = db.prepare(`SELECT COALESCE(SUM(consumed_left_pv),0) AS l, COALESCE(SUM(consumed_right_pv),0) AS r
112
- FROM binary_score_records WHERE user_id = ? AND created_at >= ?`)
113
- .get(userId, since);
114
- const leftPv = Number(u?.total_left_pv ?? 0) + Number(w.l);
115
- const rightPv = Number(u?.total_right_pv ?? 0) + Number(w.r);
116
- side = leftPv <= rightPv ? 'left' : 'right';
117
- }
118
- else {
119
- side = (Number(u?.left_count ?? 0) <= Number(u?.right_count ?? 0)) ? 'left' : 'right';
120
- }
121
- }
122
- const completed = db.prepare("SELECT COUNT(*) as n FROM orders WHERE buyer_id = ? AND status = 'completed'").get(userId).n;
123
- const override = db.prepare("SELECT l1_share_override FROM users WHERE id = ?").get(userId)?.l1_share_override ?? 0;
103
+ // pre-public 去左右码:分享链接不再计算/携带 side(放置侧别由注册时系统自动决定)
104
+ const completed = (await dbOne("SELECT COUNT(*) as n FROM orders WHERE buyer_id = ? AND status = 'completed'", [userId])).n;
105
+ const override = (await dbOne("SELECT l1_share_override FROM users WHERE id = ?", [userId]))?.l1_share_override ?? 0;
124
106
  const canL1 = override === 1 || (override === 0 && completed > 0);
125
107
  const rate = Number(product.commission_rate ?? 0);
126
- const link = `/?ref=${userId}&side=${side}#order-product/${productId}`;
108
+ // share ref uses permanent_code ONLY — never the raw user_id; fail clearly if it's missing.
109
+ const refCode = (await dbOne("SELECT permanent_code FROM users WHERE id = ?", [userId]))?.permanent_code || null;
110
+ if (!refCode)
111
+ return void res.status(409).json({ error: '邀请码暂不可用,请刷新或联系支持', error_code: 'PERMANENT_CODE_MISSING' });
112
+ const link = `/?ref=${refCode}#order-product/${productId}`;
127
113
  res.json({
128
114
  product: { id: product.id, title: product.title, price: product.price, commission_rate: rate },
129
115
  share_link: link,
130
116
  full_url_hint: 'Prepend webaz.xyz (production) to get the absolute URL',
131
- side,
132
- binary_explanation: `New user via this link → placed in your ${side === 'left' ? '🔵 left' : '🟢 right'} subtree (tail anchor)`,
117
+ placement_note: 'New user via this link → placement is recorded automatically by the system (no left/right choice).',
133
118
  commission_eligibility: canL1
134
119
  ? `You will earn 3-tier commission: L1=${(rate * 0.70 * 100).toFixed(1)}% L2=${(rate * 0.20 * 100).toFixed(1)}% L3=${(rate * 0.10 * 100).toFixed(1)}% of sale price`
135
120
  : 'You are NOT verified yet (need 1 completed purchase). 3-tier commission will be skipped, but points-matching still builds.',
@@ -1,3 +1,4 @@
1
+ import { dbOne } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
1
2
  export function registerReputationRoutes(app, deps) {
2
3
  const { db, auth, getReputation, getSellerMetrics } = deps;
3
4
  app.get('/api/reputation', (req, res) => {
@@ -16,9 +17,9 @@ export function registerReputationRoutes(app, deps) {
16
17
  metrics: getSellerMetrics(user.id),
17
18
  });
18
19
  });
19
- app.get('/api/reputation/:userId', (req, res) => {
20
+ app.get('/api/reputation/:userId', async (req, res) => {
20
21
  const rep = getReputation(db, req.params.userId);
21
- const decayRow = db.prepare(`SELECT last_decay_at FROM reputation_scores WHERE user_id = ?`).get(req.params.userId);
22
+ const decayRow = await dbOne(`SELECT last_decay_at FROM reputation_scores WHERE user_id = ?`, [req.params.userId]);
22
23
  res.json({
23
24
  level: rep.level,
24
25
  total_points: rep.total_points,