@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,18 +1,19 @@
1
+ import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
1
2
  export function registerShareablesInteractionsRoutes(app, deps) {
2
3
  const { db, auth, generateId, rateLimitOk, piiSanitize, detectFraud, commentBlocklistHit, llmModerateComment, parseMentions, notifyMentions } = deps;
3
- app.post('/api/shareables/:id/click', (req, res) => {
4
+ app.post('/api/shareables/:id/click', async (req, res) => {
4
5
  // 点击计数(不要求 auth — 任何人点击外链都计数)
5
- db.prepare("UPDATE shareables SET click_count = click_count + 1 WHERE id = ? AND status = 'active'").run(req.params.id);
6
+ await dbRun("UPDATE shareables SET click_count = click_count + 1 WHERE id = ? AND status = 'active'", [req.params.id]);
6
7
  res.json({ ok: true });
7
8
  });
8
9
  // LIKE 系统:toggle 点赞(每用户对每 shareable 一票;不能给自己点)
9
- app.post('/api/shareables/:id/like', (req, res) => {
10
+ app.post('/api/shareables/:id/like', async (req, res) => {
10
11
  const user = auth(req, res);
11
12
  if (!user)
12
13
  return;
13
14
  if (!rateLimitOk(`like:${user.id}`, 60, 60_000))
14
15
  return void res.status(429).json({ error: '点赞过于频繁' });
15
- const sh = db.prepare("SELECT id, owner_id, related_product_id, status FROM shareables WHERE id = ?").get(req.params.id);
16
+ const sh = await dbOne("SELECT id, owner_id, related_product_id, status FROM shareables WHERE id = ?", [req.params.id]);
16
17
  if (!sh)
17
18
  return void res.status(404).json({ error: 'shareable 不存在' });
18
19
  if (sh.status !== 'active')
@@ -20,7 +21,7 @@ export function registerShareablesInteractionsRoutes(app, deps) {
20
21
  if (sh.owner_id === user.id)
21
22
  return void res.json({ error: '不能给自己点赞' });
22
23
  // P1 Sybil 软门槛:至少完成过 1 笔订单(不限购买该商品,只需活跃用户)
23
- const completed = db.prepare("SELECT COUNT(1) as n FROM orders WHERE buyer_id = ? AND status = 'completed'").get(user.id).n;
24
+ const completed = (await dbOne("SELECT COUNT(1) as n FROM orders WHERE buyer_id = ? AND status = 'completed'", [user.id])).n;
24
25
  if (completed < 1)
25
26
  return void res.json({ error: '完成首笔购买后才能点赞(防止刷赞)' });
26
27
  // P0 fix:SELECT existing 进 transaction
@@ -42,39 +43,38 @@ export function registerShareablesInteractionsRoutes(app, deps) {
42
43
  liked = true;
43
44
  }
44
45
  })();
45
- const newCount = db.prepare('SELECT like_count FROM shareables WHERE id = ?').get(req.params.id).like_count;
46
+ const newCount = (await dbOne('SELECT like_count FROM shareables WHERE id = ?', [req.params.id])).like_count;
46
47
  // 通知 owner(仅新增点赞,避免取消时打扰)
47
48
  if (liked) {
48
49
  try {
49
- db.prepare(`INSERT INTO notifications (id, user_id, type, title, body, created_at)
50
- VALUES (?,?,'shareable_like',?,?,datetime('now'))`)
51
- .run(generateId('ntf'), sh.owner_id, `❤️ 收到点赞`, `分享 #${req.params.id.slice(-8)} 被点赞(累计 ${newCount})`);
50
+ await dbRun(`INSERT INTO notifications (id, user_id, type, title, body, created_at)
51
+ VALUES (?,?,'shareable_like',?,?,datetime('now'))`, [generateId('ntf'), sh.owner_id, `❤️ 收到点赞`, `分享 #${req.params.id.slice(-8)} 被点赞(累计 ${newCount})`]);
52
52
  }
53
53
  catch { }
54
54
  }
55
55
  res.json({ liked, like_count: newCount });
56
56
  });
57
57
  // W6 笔记评论 — 楼中楼 1 层(root + replies)
58
- app.get('/api/shareables/:id/comments', (req, res) => {
59
- const sh = db.prepare(`SELECT id FROM shareables WHERE id = ?`).get(req.params.id);
58
+ app.get('/api/shareables/:id/comments', async (req, res) => {
59
+ const sh = await dbOne(`SELECT id FROM shareables WHERE id = ?`, [req.params.id]);
60
60
  if (!sh)
61
61
  return void res.status(404).json({ error: 'shareable 不存在' });
62
62
  const limit = Math.min(100, Math.max(10, Number(req.query.limit) || 50));
63
63
  const sort = String(req.query.sort || 'newest');
64
64
  const orderBy = sort === 'top' ? 'c.likes DESC, c.created_at DESC' : 'c.created_at DESC';
65
- const roots = db.prepare(`
65
+ const roots = await dbAll(`
66
66
  SELECT c.*, u.handle, u.name, u.role
67
67
  FROM shareable_comments c LEFT JOIN users u ON u.id = c.commenter_id
68
68
  WHERE c.shareable_id = ? AND c.parent_id IS NULL AND c.flagged = 0
69
69
  ORDER BY ${orderBy} LIMIT ?
70
- `).all(sh.id, limit);
70
+ `, [sh.id, limit]);
71
71
  const rootIds = roots.map(r => r.id);
72
- const replies = rootIds.length > 0 ? db.prepare(`
72
+ const replies = rootIds.length > 0 ? await dbAll(`
73
73
  SELECT c.*, u.handle, u.name, u.role
74
74
  FROM shareable_comments c LEFT JOIN users u ON u.id = c.commenter_id
75
75
  WHERE c.parent_id IN (${rootIds.map(() => '?').join(',')}) AND c.flagged = 0
76
76
  ORDER BY c.created_at ASC
77
- `).all(...rootIds) : [];
77
+ `, rootIds) : [];
78
78
  const replyMap = new Map();
79
79
  for (const r of replies) {
80
80
  const pid = String(r.parent_id);
@@ -83,21 +83,21 @@ export function registerShareablesInteractionsRoutes(app, deps) {
83
83
  replyMap.set(pid, arr);
84
84
  }
85
85
  const items = roots.map(r => ({ ...r, replies: replyMap.get(r.id) || [] }));
86
- const total = db.prepare(`SELECT COUNT(*) as n FROM shareable_comments WHERE shareable_id = ? AND flagged = 0`).get(sh.id).n;
86
+ const total = (await dbOne(`SELECT COUNT(*) as n FROM shareable_comments WHERE shareable_id = ? AND flagged = 0`, [sh.id])).n;
87
87
  res.json({ items, total, sort });
88
88
  });
89
89
  app.post('/api/shareables/:id/comments', async (req, res) => {
90
90
  const user = auth(req, res);
91
91
  if (!user)
92
92
  return;
93
- const sh = db.prepare(`SELECT id, owner_id, status FROM shareables WHERE id = ?`).get(req.params.id);
93
+ const sh = await dbOne(`SELECT id, owner_id, status FROM shareables WHERE id = ?`, [req.params.id]);
94
94
  if (!sh)
95
95
  return void res.status(404).json({ error: 'shareable 不存在' });
96
96
  if (sh.status !== 'active')
97
97
  return void res.status(400).json({ error: 'shareable 已下架' });
98
98
  const parentId = req.body?.parent_id ? String(req.body.parent_id) : null;
99
99
  if (parentId) {
100
- const parent = db.prepare(`SELECT id, parent_id FROM shareable_comments WHERE id = ? AND shareable_id = ?`).get(parentId, sh.id);
100
+ const parent = await dbOne(`SELECT id, parent_id FROM shareable_comments WHERE id = ? AND shareable_id = ?`, [parentId, sh.id]);
101
101
  if (!parent)
102
102
  return void res.status(404).json({ error: '父评论不存在' });
103
103
  if (parent.parent_id)
@@ -120,14 +120,13 @@ export function registerShareablesInteractionsRoutes(app, deps) {
120
120
  // 同仲裁评论:flagged 给管理员,flag_reasons 给反诈;用 rawBody
121
121
  const reasons = detectFraud(rawBody);
122
122
  const cid = generateId('scom');
123
- db.prepare(`INSERT INTO shareable_comments (id, shareable_id, commenter_id, parent_id, body, flag_reasons) VALUES (?,?,?,?,?,?)`)
124
- .run(cid, sh.id, user.id, parentId, body, reasons.length ? JSON.stringify(reasons) : null);
123
+ await dbRun(`INSERT INTO shareable_comments (id, shareable_id, commenter_id, parent_id, body, flag_reasons) VALUES (?,?,?,?,?,?)`, [cid, sh.id, user.id, parentId, body,
124
+ reasons.length ? JSON.stringify(reasons) : null]);
125
125
  // 通知作者(自己评论自己除外)+ W9 action
126
126
  if (sh.owner_id !== user.id) {
127
127
  try {
128
128
  const actions = JSON.stringify([{ kind: 'navigate', label: '查看笔记', href: `#note/${sh.id}`, style: 'primary' }]);
129
- db.prepare(`INSERT INTO notifications (id, user_id, type, title, body, order_id, actions) VALUES (?,?,?,?,?,?,?)`)
130
- .run(generateId('ntf'), sh.owner_id, 'note_comment', parentId ? '💬 笔记评论新回复' : '💬 笔记新评论', body.slice(0, 80), null, actions);
129
+ await dbRun(`INSERT INTO notifications (id, user_id, type, title, body, order_id, actions) VALUES (?,?,?,?,?,?,?)`, [generateId('ntf'), sh.owner_id, 'note_comment', parentId ? '💬 笔记评论新回复' : '💬 笔记新评论', body.slice(0, 80), null, actions]);
131
130
  }
132
131
  catch (e) {
133
132
  console.warn('[notif note_comment]', e.message);
@@ -139,43 +138,43 @@ export function registerShareablesInteractionsRoutes(app, deps) {
139
138
  res.json({ success: true, id: cid, flag_reasons: reasons, mentions: commentMentions.map(m => m.handle) });
140
139
  });
141
140
  // 查询单个 shareable 我是否点赞过(用于 UI 状态)
142
- app.get('/api/shareables/:id/like-status', (req, res) => {
141
+ app.get('/api/shareables/:id/like-status', async (req, res) => {
143
142
  const user = auth(req, res);
144
143
  if (!user)
145
144
  return;
146
- const row = db.prepare('SELECT id FROM shareable_likes WHERE shareable_id = ? AND user_id = ?').get(req.params.id, user.id);
147
- const count = db.prepare('SELECT like_count FROM shareables WHERE id = ?').get(req.params.id)?.like_count ?? 0;
145
+ const row = await dbOne('SELECT id FROM shareable_likes WHERE shareable_id = ? AND user_id = ?', [req.params.id, user.id]);
146
+ const count = (await dbOne('SELECT like_count FROM shareables WHERE id = ?', [req.params.id]))?.like_count ?? 0;
148
147
  res.json({ liked: !!row, like_count: count });
149
148
  });
150
149
  // ─── 收藏 Bookmarks(小红书风格"收藏" tab)── 2026-05-22 audit ─────
151
150
  // POST 切换:未收藏 → 加 / 已收藏 → 删(toggle 模式)
152
- app.post('/api/shareables/:id/bookmark', (req, res) => {
151
+ app.post('/api/shareables/:id/bookmark', async (req, res) => {
153
152
  const user = auth(req, res);
154
153
  if (!user)
155
154
  return;
156
155
  const id = String(req.params.id);
157
156
  // 确认 shareable 存在 + active
158
- const sh = db.prepare("SELECT id FROM shareables WHERE id = ? AND status = 'active'").get(id);
157
+ const sh = await dbOne("SELECT id FROM shareables WHERE id = ? AND status = 'active'", [id]);
159
158
  if (!sh)
160
159
  return void res.status(404).json({ error: 'not_found' });
161
- const existing = db.prepare('SELECT id FROM shareable_bookmarks WHERE shareable_id = ? AND user_id = ?').get(id, user.id);
160
+ const existing = await dbOne('SELECT id FROM shareable_bookmarks WHERE shareable_id = ? AND user_id = ?', [id, user.id]);
162
161
  if (existing) {
163
- db.prepare('DELETE FROM shareable_bookmarks WHERE id = ?').run(existing.id);
162
+ await dbRun('DELETE FROM shareable_bookmarks WHERE id = ?', [existing.id]);
164
163
  return void res.json({ bookmarked: false });
165
164
  }
166
- db.prepare('INSERT INTO shareable_bookmarks (id, shareable_id, user_id) VALUES (?, ?, ?)').run(generateId('bm'), id, user.id);
165
+ await dbRun('INSERT INTO shareable_bookmarks (id, shareable_id, user_id) VALUES (?, ?, ?)', [generateId('bm'), id, user.id]);
167
166
  res.json({ bookmarked: true });
168
167
  });
169
168
  // 查 bookmark 状态
170
- app.get('/api/shareables/:id/bookmark-status', (req, res) => {
169
+ app.get('/api/shareables/:id/bookmark-status', async (req, res) => {
171
170
  const user = auth(req, res);
172
171
  if (!user)
173
172
  return;
174
- const row = db.prepare('SELECT id FROM shareable_bookmarks WHERE shareable_id = ? AND user_id = ?').get(req.params.id, user.id);
173
+ const row = await dbOne('SELECT id FROM shareable_bookmarks WHERE shareable_id = ? AND user_id = ?', [req.params.id, user.id]);
175
174
  res.json({ bookmarked: !!row });
176
175
  });
177
176
  // 我收藏过的 shareables(仅 owner 自己可见)
178
- app.get('/api/users/:id/bookmarked-shareables', (req, res) => {
177
+ app.get('/api/users/:id/bookmarked-shareables', async (req, res) => {
179
178
  const me = auth(req, res);
180
179
  if (!me)
181
180
  return;
@@ -185,7 +184,7 @@ export function registerShareablesInteractionsRoutes(app, deps) {
185
184
  ownerId = me.id;
186
185
  if (!ownerId)
187
186
  return void res.status(403).json({ error: 'only owner can view bookmarks' });
188
- const rows = db.prepare(`
187
+ const rows = await dbAll(`
189
188
  SELECT s.id, s.owner_id, s.owner_code, s.type, s.external_url, s.external_platform,
190
189
  s.thumbnail_url, s.title, s.description, s.photo_hashes, s.related_product_id, s.related_anchor,
191
190
  s.click_count, s.like_count, s.created_at,
@@ -196,7 +195,7 @@ export function registerShareablesInteractionsRoutes(app, deps) {
196
195
  LEFT JOIN products p ON p.id = s.related_product_id
197
196
  WHERE b.user_id = ? AND s.status = 'active'
198
197
  ORDER BY b.created_at DESC LIMIT 100
199
- `).all(ownerId);
198
+ `, [ownerId]);
200
199
  for (const r of rows) {
201
200
  if (typeof r.photo_hashes === 'string') {
202
201
  try {
@@ -1,6 +1,7 @@
1
1
  import express from 'express';
2
2
  import { writeNotePhoto, readNotePhoto, noteBlobExists, NOTE_PHOTO_MAX_BYTES, NOTE_PHOTO_ALLOWED_MIME } from '../../layer2-business/L2-notes/note-photo-storage.js';
3
3
  import { retireAnchorsByTarget } from '../../layer2-business/L2-anchor-registry/anchor-registry.js';
4
+ import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
4
5
  export const SHAREABLE_DAILY_LIMIT = 10;
5
6
  export function registerShareablesRoutes(app, deps) {
6
7
  const { db, auth, getUser, generateId, lightAuthGuard, detectExternalPlatform, noteAuthenticityBadges, parseHashtags, parseMentions, notifyMentions, flagNewAccountShareable, refreshProductSharerCount } = deps;
@@ -47,7 +48,7 @@ export function registerShareablesRoutes(app, deps) {
47
48
  }
48
49
  });
49
50
  // 创建 shareable — 双路径:笔记模式 / 外链或 native_text 模式
50
- app.post('/api/shareables', (req, res) => {
51
+ app.post('/api/shareables', async (req, res) => {
51
52
  const me = auth(req, res);
52
53
  if (!me)
53
54
  return;
@@ -61,7 +62,7 @@ export function registerShareablesRoutes(app, deps) {
61
62
  // ─── 笔记模式专属校验 ───────────────────────────────────────
62
63
  if (!related_order_id)
63
64
  return void res.json({ error: '笔记必须关联订单(你购买过的 completed 订单)' });
64
- const order = db.prepare(`SELECT id, buyer_id, seller_id, product_id, status FROM orders WHERE id = ?`).get(related_order_id);
65
+ const order = await dbOne(`SELECT id, buyer_id, seller_id, product_id, status FROM orders WHERE id = ?`, [related_order_id]);
65
66
  if (!order)
66
67
  return void res.status(404).json({ error: '订单不存在' });
67
68
  if (order.buyer_id !== me.id)
@@ -69,7 +70,7 @@ export function registerShareablesRoutes(app, deps) {
69
70
  if (order.status !== 'completed')
70
71
  return void res.json({ error: '订单完成后才能发笔记' });
71
72
  // 每订单 1 篇原创(转发不算 — 转发用 parent_id)
72
- const dupOrder = db.prepare(`SELECT id FROM shareables WHERE owner_id = ? AND related_order_id = ? AND type = 'note' AND parent_id IS NULL AND status != 'removed' LIMIT 1`).get(me.id, related_order_id);
73
+ const dupOrder = await dbOne(`SELECT id FROM shareables WHERE owner_id = ? AND related_order_id = ? AND type = 'note' AND parent_id IS NULL AND status != 'removed' LIMIT 1`, [me.id, related_order_id]);
73
74
  if (dupOrder && !parent_id)
74
75
  return void res.json({ error: '该订单已发过原创笔记', existing_id: dupOrder.id });
75
76
  if (trimText.length < 30)
@@ -89,7 +90,7 @@ export function registerShareablesRoutes(app, deps) {
89
90
  // 图 hash 跨笔记唯一(防剽窃)— 审计修 C-1
90
91
  const hashList = photo_hashes;
91
92
  for (const h of hashList) {
92
- const existing = db.prepare(`SELECT shareable_id FROM note_photo_index WHERE hash = ?`).get(h);
93
+ const existing = await dbOne(`SELECT shareable_id FROM note_photo_index WHERE hash = ?`, [h]);
93
94
  if (existing && existing.shareable_id) {
94
95
  return void res.json({
95
96
  error: `图片已被其它笔记使用(疑似剽窃):${h.slice(0, 12)}…`,
@@ -100,25 +101,27 @@ export function registerShareablesRoutes(app, deps) {
100
101
  const productId = order.product_id;
101
102
  // parent_id 校验(转发链)
102
103
  if (parent_id) {
103
- const parent = db.prepare(`SELECT id, related_product_id FROM shareables WHERE id = ? AND status != 'removed'`).get(parent_id);
104
+ const parent = await dbOne(`SELECT id, related_product_id FROM shareables WHERE id = ? AND status != 'removed'`, [parent_id]);
104
105
  if (!parent)
105
106
  return void res.json({ error: '原笔记不存在' });
106
107
  if (parent.related_product_id !== productId)
107
108
  return void res.json({ error: '转发必须基于同一商品的笔记' });
108
109
  }
109
110
  // 日上限
110
- const todayCount = db.prepare(`SELECT COUNT(*) as n FROM shareables WHERE owner_id = ? AND created_at > datetime('now', '-1 day')`).get(me.id).n;
111
+ const todayCount = (await dbOne(`SELECT COUNT(*) as n FROM shareables WHERE owner_id = ? AND created_at > datetime('now', '-1 day')`, [me.id])).n;
111
112
  if (todayCount >= SHAREABLE_DAILY_LIMIT)
112
113
  return void res.json({ error: `每日上限 ${SHAREABLE_DAILY_LIMIT} 条,请明天再来` });
113
114
  const id = generateId('shr');
114
- const ownerCode = db.prepare("SELECT permanent_code FROM users WHERE id = ?").get(me.id)?.permanent_code || null;
115
- db.prepare(`INSERT INTO shareables
115
+ const ownerCode = (await dbOne("SELECT permanent_code FROM users WHERE id = ?", [me.id]))?.permanent_code || null;
116
+ await dbRun(`INSERT INTO shareables
116
117
  (id, owner_id, type, native_text, title, description, related_product_id, related_order_id, parent_id, photo_hashes, owner_code)
117
- VALUES (?,?,?,?,?,?,?,?,?,?,?)`)
118
- .run(id, me.id, 'note', trimText, (title || null), (description || null), productId, related_order_id, parent_id || null, JSON.stringify(hashList), ownerCode);
118
+ VALUES (?,?,?,?,?,?,?,?,?,?,?)`, [id, me.id, 'note', trimText,
119
+ (title || null), (description || null),
120
+ productId, related_order_id, parent_id || null,
121
+ JSON.stringify(hashList), ownerCode]);
119
122
  for (const h of hashList) {
120
123
  try {
121
- db.prepare(`INSERT OR IGNORE INTO note_photo_index (hash, shareable_id) VALUES (?,?)`).run(h, id);
124
+ await dbRun(`INSERT OR IGNORE INTO note_photo_index (hash, shareable_id) VALUES (?,?)`, [h, id]);
122
125
  }
123
126
  catch { }
124
127
  }
@@ -126,7 +129,7 @@ export function registerShareablesRoutes(app, deps) {
126
129
  const tags = parseHashtags((title || '') + ' ' + trimText);
127
130
  for (const tg of tags) {
128
131
  try {
129
- db.prepare(`INSERT OR IGNORE INTO shareable_tags (shareable_id, tag) VALUES (?,?)`).run(id, tg);
132
+ await dbRun(`INSERT OR IGNORE INTO shareable_tags (shareable_id, tag) VALUES (?,?)`, [id, tg]);
130
133
  }
131
134
  catch { }
132
135
  }
@@ -148,16 +151,16 @@ export function registerShareablesRoutes(app, deps) {
148
151
  return void res.json({ error: '标题不能超过 100 字' });
149
152
  if ((description || '').length > 200)
150
153
  return void res.json({ error: '描述不能超过 200 字' });
151
- const todayCount = db.prepare(`SELECT COUNT(*) as n FROM shareables WHERE owner_id = ? AND created_at > datetime('now', '-1 day')`).get(me.id).n;
154
+ const todayCount = (await dbOne(`SELECT COUNT(*) as n FROM shareables WHERE owner_id = ? AND created_at > datetime('now', '-1 day')`, [me.id])).n;
152
155
  if (todayCount >= SHAREABLE_DAILY_LIMIT)
153
156
  return void res.json({ error: `每日上限 ${SHAREABLE_DAILY_LIMIT} 条,请明天再来` });
154
157
  if (trimUrl) {
155
- const dup = db.prepare(`SELECT id FROM shareables WHERE owner_id = ? AND external_url = ? AND status != 'removed' LIMIT 1`).get(me.id, trimUrl);
158
+ const dup = await dbOne(`SELECT id FROM shareables WHERE owner_id = ? AND external_url = ? AND status != 'removed' LIMIT 1`, [me.id, trimUrl]);
156
159
  if (dup)
157
160
  return void res.json({ error: '已存在相同链接,请编辑现有条目', existing_id: dup.id });
158
161
  }
159
162
  if (related_product_id) {
160
- const p = db.prepare("SELECT id FROM products WHERE id = ?").get(related_product_id);
163
+ const p = await dbOne("SELECT id FROM products WHERE id = ?", [related_product_id]);
161
164
  if (!p)
162
165
  return void res.json({ error: '关联商品不存在' });
163
166
  }
@@ -165,58 +168,59 @@ export function registerShareablesRoutes(app, deps) {
165
168
  const { type, platform, video_id, thumbnail } = trimUrl
166
169
  ? detectExternalPlatform(trimUrl)
167
170
  : { type: 'native_text', platform: 'native', video_id: undefined, thumbnail: undefined };
168
- const ownerCode = db.prepare("SELECT permanent_code FROM users WHERE id = ?").get(me.id)?.permanent_code || null;
169
- db.prepare(`INSERT INTO shareables (id, owner_id, type, external_url, external_platform, external_video_id, thumbnail_url, title, description, native_text, related_product_id, related_anchor, owner_code)
170
- VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)`)
171
- .run(id, me.id, type, trimUrl || null, platform, video_id || null, thumbnail || null, (title || null), (description || null), trimText || null, related_product_id || null, related_anchor || null, ownerCode);
171
+ const ownerCode = (await dbOne("SELECT permanent_code FROM users WHERE id = ?", [me.id]))?.permanent_code || null;
172
+ await dbRun(`INSERT INTO shareables (id, owner_id, type, external_url, external_platform, external_video_id, thumbnail_url, title, description, native_text, related_product_id, related_anchor, owner_code)
173
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)`, [id, me.id, type, trimUrl || null, platform, video_id || null, thumbnail || null,
174
+ (title || null), (description || null), trimText || null,
175
+ related_product_id || null, related_anchor || null, ownerCode]);
172
176
  flagNewAccountShareable(id, me.id);
173
177
  if (related_product_id)
174
178
  refreshProductSharerCount(related_product_id);
175
179
  res.json({ ok: true, id, type, platform, thumbnail, owner_code: ownerCode });
176
180
  });
177
- app.get('/api/shareables/me', (req, res) => {
181
+ app.get('/api/shareables/me', async (req, res) => {
178
182
  const me = auth(req, res);
179
183
  if (!me)
180
184
  return;
181
- const rows = db.prepare(`
185
+ const rows = await dbAll(`
182
186
  SELECT s.*, p.title as product_title FROM shareables s
183
187
  LEFT JOIN products p ON p.id = s.related_product_id
184
188
  WHERE s.owner_id = ? AND s.status != 'removed'
185
189
  ORDER BY s.created_at DESC LIMIT 100
186
- `).all(me.id);
190
+ `, [me.id]);
187
191
  res.json({ shareables: rows });
188
192
  });
189
193
  // 里程碑 L3:创作者贡献仪表盘
190
- app.get('/api/creator/stats', (req, res) => {
194
+ app.get('/api/creator/stats', async (req, res) => {
191
195
  const me = auth(req, res);
192
196
  if (!me)
193
197
  return;
194
198
  const meId = me.id;
195
- const shareables = db.prepare(`
199
+ const shareables = await dbAll(`
196
200
  SELECT id, related_product_id, click_count, unique_click_count, flag_new_account, created_at
197
201
  FROM shareables WHERE owner_id = ? AND status != 'removed'
198
- `).all(meId);
202
+ `, [meId]);
199
203
  const totalShares = shareables.length;
200
204
  const productShares = shareables.filter(s => s.related_product_id);
201
205
  const uniqueProducts = new Set(productShares.map(s => s.related_product_id)).size;
202
206
  const rawClicks = shareables.reduce((a, s) => a + (s.click_count || 0), 0);
203
207
  const uniqueClicks = shareables.reduce((a, s) => a + (s.unique_click_count || 0), 0);
204
208
  const newAccountFlagged = shareables.filter(s => s.flag_new_account).length;
205
- const conversions = db.prepare(`
209
+ const conversions = (await dbOne(`
206
210
  SELECT COUNT(*) as n FROM product_share_attribution psa
207
211
  JOIN orders o ON o.product_id = psa.product_id AND o.buyer_id = psa.recipient_id
208
212
  WHERE psa.sharer_id = ? AND o.status = 'completed' AND o.created_at >= psa.created_at
209
- `).get(meId).n;
210
- const l1Earn = db.prepare(`
213
+ `, [meId])).n;
214
+ const l1Earn = (await dbOne(`
211
215
  SELECT COALESCE(SUM(amount), 0) as total
212
216
  FROM commission_records WHERE beneficiary_id = ? AND level = 1
213
- `).get(meId).total;
217
+ `, [meId])).total;
214
218
  // #7 按 source_type 分项 — 笔记 vs 普通分享 vs sponsor 链
215
- const bySource = db.prepare(`
219
+ const bySource = await dbAll(`
216
220
  SELECT source_type, COALESCE(SUM(amount), 0) as total, COUNT(*) as cnt
217
221
  FROM commission_records WHERE beneficiary_id = ?
218
222
  GROUP BY source_type
219
- `).all(meId);
223
+ `, [meId]);
220
224
  const sourceBreakdown = { note: 0, link: 0, sponsor: 0 };
221
225
  const sourceCntBreakdown = { note: 0, link: 0, sponsor: 0 };
222
226
  for (const r of bySource) {
@@ -225,13 +229,13 @@ export function registerShareablesRoutes(app, deps) {
225
229
  sourceCntBreakdown[k] += Number(r.cnt) || 0;
226
230
  }
227
231
  // 30 天点击趋势
228
- const trend30d = db.prepare(`
232
+ const trend30d = await dbAll(`
229
233
  SELECT substr(created_at, 1, 10) as day, COUNT(*) as raw_clicks, COUNT(DISTINCT ip_hash || ':' || ua_hash) as unique_clicks
230
234
  FROM shareable_click_log
231
235
  WHERE shareable_id IN (SELECT id FROM shareables WHERE owner_id = ?)
232
236
  AND created_at > datetime('now', '-30 days')
233
237
  GROUP BY day ORDER BY day ASC
234
- `).all(meId);
238
+ `, [meId]);
235
239
  res.json({
236
240
  shares: { total: totalShares, product_count: uniqueProducts, new_account_flagged: newAccountFlagged },
237
241
  clicks: { raw: rawClicks, unique: uniqueClicks, raw_to_unique_ratio: rawClicks > 0 ? Math.round(uniqueClicks / rawClicks * 100) / 100 : null },
@@ -251,11 +255,11 @@ export function registerShareablesRoutes(app, deps) {
251
255
  });
252
256
  });
253
257
  // 策展引用:按 click*1 + like*3 + induced_orders*10 加权排序,取 top 10
254
- app.get('/api/shareables/by-product/:pid', (req, res) => {
258
+ app.get('/api/shareables/by-product/:pid', async (req, res) => {
255
259
  const user = auth(req, res);
256
260
  if (!user)
257
261
  return;
258
- const rows = db.prepare(`
262
+ const rows = await dbAll(`
259
263
  SELECT * FROM (
260
264
  SELECT s.*, u.name as owner_name, u.handle as owner_handle,
261
265
  (SELECT COUNT(DISTINCT o.id) FROM orders o
@@ -268,29 +272,29 @@ export function registerShareablesRoutes(app, deps) {
268
272
  ) sub
269
273
  ORDER BY (click_count * 1.0 + like_count * 3.0 + induced_orders * 10.0) DESC, created_at DESC
270
274
  LIMIT 10
271
- `).all(req.params.pid);
275
+ `, [req.params.pid]);
272
276
  for (const r of rows) {
273
277
  r.badges = noteAuthenticityBadges(r);
274
278
  }
275
279
  res.json({ shareables: rows });
276
280
  });
277
- app.get('/api/shareables/by-anchor/:anchor', (req, res) => {
281
+ app.get('/api/shareables/by-anchor/:anchor', async (req, res) => {
278
282
  const user = auth(req, res);
279
283
  if (!user)
280
284
  return;
281
- const rows = db.prepare(`
285
+ const rows = await dbAll(`
282
286
  SELECT s.*, u.name as owner_name FROM shareables s
283
287
  LEFT JOIN users u ON u.id = s.owner_id
284
288
  WHERE s.related_anchor = ? AND s.status = 'active'
285
289
  ORDER BY s.created_at DESC LIMIT 50
286
- `).all(req.params.anchor);
290
+ `, [req.params.anchor]);
287
291
  res.json({ shareables: rows });
288
292
  });
289
293
  // Phase D2 笔记 list — 公开 feed,3 种 sort
290
294
  // sort=newest: created_at DESC
291
295
  // sort=trending: (likes*2 + click/10 + freshness/(age_hours+1)) DESC
292
296
  // sort=following: 需登录,仅显示 follows.followee_id 的笔记
293
- app.get('/api/notes', (req, res) => {
297
+ app.get('/api/notes', async (req, res) => {
294
298
  const limit = Math.min(50, Math.max(1, Number(req.query.limit) || 20));
295
299
  const cursor = req.query.cursor ? String(req.query.cursor) : null;
296
300
  const sort = String(req.query.sort || 'newest');
@@ -326,7 +330,7 @@ export function registerShareablesRoutes(app, deps) {
326
330
  LIMIT ?
327
331
  `;
328
332
  args.push(limit + 1);
329
- const rows = db.prepare(sql).all(...args);
333
+ const rows = await dbAll(sql, args);
330
334
  const hasMore = rows.length > limit;
331
335
  const items = rows.slice(0, limit).map(r => {
332
336
  let photos = [];
@@ -351,16 +355,16 @@ export function registerShareablesRoutes(app, deps) {
351
355
  res.json({ items, next_cursor: nextCursor, sort });
352
356
  });
353
357
  // Phase C 笔记公开读 — 任何人可读
354
- app.get('/api/shareables/:id', (req, res) => {
358
+ app.get('/api/shareables/:id', async (req, res) => {
355
359
  const id = String(req.params.id);
356
- const row = db.prepare(`
360
+ const row = await dbOne(`
357
361
  SELECT s.id, s.owner_id, s.owner_code, s.type, s.title, s.description, s.native_text,
358
362
  s.related_product_id, s.related_order_id, s.parent_id, s.photo_hashes,
359
363
  s.click_count, s.unique_click_count, s.like_count, s.created_at, s.status,
360
364
  u.handle as owner_handle, u.name as owner_name, u.region as owner_region
361
365
  FROM shareables s LEFT JOIN users u ON u.id = s.owner_id
362
366
  WHERE s.id = ? AND s.status = 'active'
363
- `).get(id);
367
+ `, [id]);
364
368
  if (!row)
365
369
  return void res.status(404).json({ error: 'not_found' });
366
370
  let photos = [];
@@ -370,9 +374,9 @@ export function registerShareablesRoutes(app, deps) {
370
374
  catch { }
371
375
  let product = null;
372
376
  if (row.related_product_id) {
373
- product = db.prepare(`SELECT id, title, price, category, images FROM products WHERE id = ?`).get(row.related_product_id);
377
+ product = (await dbOne(`SELECT id, title, price, category, images FROM products WHERE id = ?`, [row.related_product_id])) ?? null;
374
378
  }
375
- const tags = db.prepare(`SELECT tag FROM shareable_tags WHERE shareable_id = ? ORDER BY id`).all(id).map(r => r.tag);
379
+ const tags = (await dbAll(`SELECT tag FROM shareable_tags WHERE shareable_id = ? ORDER BY id`, [id])).map(r => r.tag);
376
380
  const badges = noteAuthenticityBadges(row);
377
381
  res.json({
378
382
  id: row.id, type: row.type,
@@ -389,11 +393,11 @@ export function registerShareablesRoutes(app, deps) {
389
393
  badges,
390
394
  });
391
395
  });
392
- app.patch('/api/shareables/:id', (req, res) => {
396
+ app.patch('/api/shareables/:id', async (req, res) => {
393
397
  const me = auth(req, res);
394
398
  if (!me)
395
399
  return;
396
- const row = db.prepare("SELECT owner_id FROM shareables WHERE id = ?").get(req.params.id);
400
+ const row = await dbOne("SELECT owner_id FROM shareables WHERE id = ?", [req.params.id]);
397
401
  if (!row || row.owner_id !== me.id)
398
402
  return void res.json({ error: '无权操作' });
399
403
  const updates = [];
@@ -426,14 +430,14 @@ export function registerShareablesRoutes(app, deps) {
426
430
  return void res.json({ error: '没有可更新字段' });
427
431
  updates.push(`updated_at = datetime('now')`);
428
432
  values.push(req.params.id);
429
- db.prepare(`UPDATE shareables SET ${updates.join(', ')} WHERE id = ?`).run(...values);
433
+ await dbRun(`UPDATE shareables SET ${updates.join(', ')} WHERE id = ?`, values);
430
434
  res.json({ ok: true });
431
435
  });
432
- app.delete('/api/shareables/:id', (req, res) => {
436
+ app.delete('/api/shareables/:id', async (req, res) => {
433
437
  const me = auth(req, res);
434
438
  if (!me)
435
439
  return;
436
- const row = db.prepare("SELECT owner_id, related_product_id, like_count, status, type FROM shareables WHERE id = ?").get(req.params.id);
440
+ const row = await dbOne("SELECT owner_id, related_product_id, like_count, status, type FROM shareables WHERE id = ?", [req.params.id]);
437
441
  if (!row || row.owner_id !== me.id)
438
442
  return void res.json({ error: '无权操作' });
439
443
  if (row.status === 'removed')
@@ -0,0 +1,58 @@
1
+ import { dbOne, dbRun } from '../../layer0-foundation/L0-1-database/db.js';
2
+ export function registerShopReferralRoutes(app, deps) {
3
+ const { auth, errorRes, internalAuditorId, resolveUserRef, resolveInviteCodeRef } = deps;
4
+ app.post('/api/shop-referral/touch', async (req, res) => {
5
+ const user = auth(req, res);
6
+ if (!user)
7
+ return;
8
+ const recipientId = user.id;
9
+ const { seller_identifier, ref_code } = req.body || {};
10
+ if (!seller_identifier || typeof seller_identifier !== 'string')
11
+ return void errorRes(res, 400, 'SELLER_REQUIRED', 'seller_identifier required');
12
+ if (!ref_code || typeof ref_code !== 'string')
13
+ return void errorRes(res, 400, 'REF_REQUIRED', 'ref_code required');
14
+ // referrer = invite code ONLY (permanent_code); usr_xxx / @handle / handle rejected.
15
+ // pre-public 去左右码:旧 -L/-R 后缀仍被接受但归一化为基础码(ref.code),side 一律忽略。
16
+ const ref = resolveInviteCodeRef(ref_code);
17
+ if (!ref)
18
+ return void errorRes(res, 400, 'INVALID_REF_CODE', '邀请码无效(仅 6-7 位永久码)');
19
+ const referrerId = ref.userId;
20
+ // pre-public 去左右码:不再按 side 归属(忽略 body.side 与邀请码 -L/-R),统一存 null
21
+ const finalSide = null;
22
+ // seller 定位:个人页多形态(usr_xxx / @handle / handle / permanent_code)。
23
+ // 必须是真实 seller 店铺 —— 普通 buyer / admin / 其它角色不能被写成 shop_referral_attribution.seller_id。
24
+ const sellerId = resolveUserRef(seller_identifier);
25
+ if (!sellerId)
26
+ return void errorRes(res, 404, 'SELLER_NOT_FOUND', '店铺不存在');
27
+ const sellerRow = await dbOne("SELECT role FROM users WHERE id = ?", [sellerId]);
28
+ if (sellerRow?.role !== 'seller')
29
+ return void errorRes(res, 404, 'SELLER_NOT_FOUND', '店铺不存在');
30
+ if ([referrerId, sellerId].some(id => id === 'sys_protocol' || id === internalAuditorId)) {
31
+ return void errorRes(res, 400, 'INVALID_PARTY', '无效推荐关系');
32
+ }
33
+ // 退化关系安全跳过(不报错,不写坏数据)
34
+ if (recipientId === referrerId)
35
+ return void res.json({ ok: true, attributed: false, skipped: 'self_referral', seller_id: sellerId });
36
+ if (recipientId === sellerId)
37
+ return void res.json({ ok: true, attributed: false, skipped: 'recipient_is_seller', seller_id: sellerId });
38
+ // first-touch:已有未过期记录不覆盖;过期记录可被刷新。
39
+ const existing = await dbOne("SELECT referrer_id FROM shop_referral_attribution WHERE seller_id = ? AND recipient_id = ? AND expires_at > datetime('now')", [sellerId, recipientId]);
40
+ if (existing)
41
+ return void res.json({ ok: true, attributed: false, skipped: 'already_locked', seller_id: sellerId });
42
+ const had = await dbOne("SELECT referrer_id FROM shop_referral_attribution WHERE seller_id = ? AND recipient_id = ?", [sellerId, recipientId]);
43
+ try {
44
+ if (had) {
45
+ // 只刷新仍过期的行(WHERE 双保险:并发下另一请求先刷新 → 本次 0 行,不覆盖)
46
+ await dbRun("UPDATE shop_referral_attribution SET referrer_id = ?, ref_code = ?, side = ?, created_at = datetime('now'), expires_at = datetime('now','+30 days'), source = 'shop_referral' WHERE seller_id = ? AND recipient_id = ? AND expires_at <= datetime('now')", [referrerId, ref.code, finalSide, sellerId, recipientId]);
47
+ }
48
+ else {
49
+ await dbRun("INSERT INTO shop_referral_attribution (seller_id, recipient_id, referrer_id, ref_code, side, expires_at) VALUES (?,?,?,?,?, datetime('now','+30 days'))", [sellerId, recipientId, referrerId, ref.code, finalSide]);
50
+ }
51
+ }
52
+ catch {
53
+ // SELECT→INSERT 非原子(async seam):并发 first-touch 撞 PRIMARY KEY → 视作已锁定,不 500
54
+ return void res.json({ ok: true, attributed: false, skipped: 'already_locked', seller_id: sellerId });
55
+ }
56
+ res.json({ ok: true, attributed: true, seller_id: sellerId });
57
+ });
58
+ }