@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,5 +1,6 @@
1
1
  import { transition } from '../../layer0-foundation/L0-2-state-machine/engine.js';
2
2
  import { notifyTransition } from '../../layer2-business/L2-6-notifications/notification-engine.js';
3
+ import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
3
4
  const SH_CATEGORIES = new Set(['phone', 'computer', 'appliance', 'furniture', 'clothing', 'book', 'toy', 'sports', 'other']);
4
5
  const SH_CONDITIONS = new Set(['brand_new', 'like_new', 'lightly_used', 'well_used', 'heavily_used']);
5
6
  const SH_FULFILLMENT = new Set(['shipping', 'in_person', 'both']);
@@ -8,9 +9,11 @@ function addHours(date, hours) {
8
9
  return new Date(date.getTime() + hours * 3_600_000).toISOString();
9
10
  }
10
11
  export function registerSecondhandRoutes(app, deps) {
12
+ // db 仍保留:order 下单是 money/escrow 路径(pragma FK-OFF 窗口 + CAS + 钱包扣减),
13
+ // 不能引入 await 否则会在 FK-OFF 窗口被其他请求并发穿插;随 order/资金路径在 Phase 3 一并迁移。
11
14
  const { db, generateId, auth, errorRes } = deps;
12
15
  // 1. 发布
13
- app.post('/api/secondhand', (req, res) => {
16
+ app.post('/api/secondhand', async (req, res) => {
14
17
  const user = auth(req, res);
15
18
  if (!user)
16
19
  return;
@@ -32,12 +35,12 @@ export function registerSecondhandRoutes(app, deps) {
32
35
  const ff = SH_FULFILLMENT.has(fulfillment) ? fulfillment : 'both';
33
36
  const reg = region ? String(region).trim().slice(0, 40) : null;
34
37
  const id = generateId('shi');
35
- db.prepare(`INSERT INTO secondhand_items (id, seller_id, title, description, category, condition_grade, price, negotiable, images, region, fulfillment)
36
- VALUES (?,?,?,?,?,?,?,?,?,?,?)`).run(id, user.id, t, desc, category, condition_grade, p, negotiable ? 1 : 0, JSON.stringify(imgs), reg, ff);
38
+ await dbRun(`INSERT INTO secondhand_items (id, seller_id, title, description, category, condition_grade, price, negotiable, images, region, fulfillment)
39
+ VALUES (?,?,?,?,?,?,?,?,?,?,?)`, [id, user.id, t, desc, category, condition_grade, p, negotiable ? 1 : 0, JSON.stringify(imgs), reg, ff]);
37
40
  res.json({ success: true, id });
38
41
  });
39
42
  // 2. 列表(市场入口)
40
- app.get('/api/secondhand', (req, res) => {
43
+ app.get('/api/secondhand', async (req, res) => {
41
44
  const category = String(req.query.category || '').trim();
42
45
  const conditionList = String(req.query.condition || '').split(',').map(s => s.trim()).filter(s => SH_CONDITIONS.has(s));
43
46
  const region = String(req.query.region || '').trim();
@@ -76,18 +79,18 @@ export function registerSecondhandRoutes(app, deps) {
76
79
  // 排除自己(如登录)
77
80
  const me = (req.headers.authorization || '').replace('Bearer ', '');
78
81
  if (me) {
79
- const u = db.prepare("SELECT id FROM users WHERE api_key = ?").get(me);
82
+ const u = await dbOne("SELECT id FROM users WHERE api_key = ?", [me]);
80
83
  if (u) {
81
84
  where.push('seller_id != ?');
82
85
  args.push(u.id);
83
86
  }
84
87
  }
85
- const rows = db.prepare(`SELECT si.id, si.seller_id, si.title, si.category, si.condition_grade, si.price, si.negotiable,
88
+ const rows = await dbAll(`SELECT si.id, si.seller_id, si.title, si.category, si.condition_grade, si.price, si.negotiable,
86
89
  si.region, si.fulfillment, si.images, si.view_count, si.created_at,
87
90
  u.name as seller_name, u.handle as seller_handle
88
91
  FROM secondhand_items si JOIN users u ON u.id = si.seller_id
89
92
  WHERE ${where.join(' AND ')}
90
- ORDER BY ${orderBy} LIMIT ${limit}`).all(...args);
93
+ ORDER BY ${orderBy} LIMIT ${limit}`, args);
91
94
  for (const r of rows) {
92
95
  try {
93
96
  const arr = JSON.parse(r.images);
@@ -101,13 +104,13 @@ export function registerSecondhandRoutes(app, deps) {
101
104
  res.json({ items: rows });
102
105
  });
103
106
  // 3. 我的二手发布
104
- app.get('/api/secondhand/mine', (req, res) => {
107
+ app.get('/api/secondhand/mine', async (req, res) => {
105
108
  const user = auth(req, res);
106
109
  if (!user)
107
110
  return;
108
- const rows = db.prepare(`SELECT id, title, category, condition_grade, price, negotiable, status,
111
+ const rows = await dbAll(`SELECT id, title, category, condition_grade, price, negotiable, status,
109
112
  region, fulfillment, images, view_count, created_at, sold_at, sold_order_id
110
- FROM secondhand_items WHERE seller_id = ? ORDER BY created_at DESC LIMIT 100`).all(user.id);
113
+ FROM secondhand_items WHERE seller_id = ? ORDER BY created_at DESC LIMIT 100`, [user.id]);
111
114
  for (const r of rows) {
112
115
  try {
113
116
  const arr = JSON.parse(r.images);
@@ -118,33 +121,33 @@ export function registerSecondhandRoutes(app, deps) {
118
121
  }
119
122
  delete r.images;
120
123
  }
121
- const stats = db.prepare(`SELECT
124
+ const stats = (await dbOne(`SELECT
122
125
  SUM(CASE WHEN status='available' THEN 1 ELSE 0 END) as available_count,
123
126
  SUM(CASE WHEN status='reserved' THEN 1 ELSE 0 END) as reserved_count,
124
127
  SUM(CASE WHEN status='sold' THEN 1 ELSE 0 END) as sold_count,
125
128
  SUM(CASE WHEN status='closed' THEN 1 ELSE 0 END) as closed_count,
126
129
  COALESCE(SUM(CASE WHEN status='sold' THEN price ELSE 0 END), 0) as gross_sold_amount
127
- FROM secondhand_items WHERE seller_id = ?`).get(user.id);
130
+ FROM secondhand_items WHERE seller_id = ?`, [user.id]));
128
131
  // 估算净收入(扣 1% 协议 + 1% 基金)
129
132
  stats.estimated_earned = Math.round((stats.gross_sold_amount || 0) * 0.98 * 100) / 100;
130
133
  res.json({ items: rows, stats });
131
134
  });
132
135
  // 4. 详情(view_count++)+ 同卖家其他在售
133
- app.get('/api/secondhand/:id', (req, res) => {
134
- const row = db.prepare(`SELECT si.*, u.name as seller_name, u.handle as seller_handle, u.permanent_code as seller_code
135
- FROM secondhand_items si JOIN users u ON u.id = si.seller_id WHERE si.id = ?`).get(req.params.id);
136
+ app.get('/api/secondhand/:id', async (req, res) => {
137
+ const row = await dbOne(`SELECT si.*, u.name as seller_name, u.handle as seller_handle, u.permanent_code as seller_code
138
+ FROM secondhand_items si JOIN users u ON u.id = si.seller_id WHERE si.id = ?`, [req.params.id]);
136
139
  if (!row)
137
140
  return void res.status(404).json({ error: '物品不存在' });
138
- db.prepare('UPDATE secondhand_items SET view_count = view_count + 1 WHERE id = ?').run(req.params.id);
141
+ await dbRun('UPDATE secondhand_items SET view_count = view_count + 1 WHERE id = ?', [req.params.id]);
139
142
  try {
140
143
  row.images = JSON.parse(row.images);
141
144
  }
142
145
  catch {
143
146
  row.images = [];
144
147
  }
145
- const sellerOthers = db.prepare(`SELECT id, title, category, condition_grade, price, images, region
148
+ const sellerOthers = await dbAll(`SELECT id, title, category, condition_grade, price, images, region
146
149
  FROM secondhand_items WHERE seller_id = ? AND status='available' AND id != ?
147
- ORDER BY created_at DESC LIMIT 6`).all(row.seller_id, req.params.id);
150
+ ORDER BY created_at DESC LIMIT 6`, [row.seller_id, req.params.id]);
148
151
  for (const o of sellerOthers) {
149
152
  try {
150
153
  const arr = JSON.parse(o.images);
@@ -158,11 +161,11 @@ export function registerSecondhandRoutes(app, deps) {
158
161
  res.json({ item: row, seller_others: sellerOthers });
159
162
  });
160
163
  // 5. 编辑(仅 owner;可改 price / description / negotiable / status / fulfillment)
161
- app.patch('/api/secondhand/:id', (req, res) => {
164
+ app.patch('/api/secondhand/:id', async (req, res) => {
162
165
  const user = auth(req, res);
163
166
  if (!user)
164
167
  return;
165
- const item = db.prepare('SELECT * FROM secondhand_items WHERE id = ?').get(req.params.id);
168
+ const item = await dbOne('SELECT * FROM secondhand_items WHERE id = ?', [req.params.id]);
166
169
  if (!item)
167
170
  return void res.status(404).json({ error: '物品不存在' });
168
171
  if (item.seller_id !== user.id)
@@ -210,10 +213,10 @@ export function registerSecondhandRoutes(app, deps) {
210
213
  return void res.status(400).json({ error: '无字段更新' });
211
214
  sets.push("updated_at = datetime('now')");
212
215
  args.push(req.params.id);
213
- db.prepare(`UPDATE secondhand_items SET ${sets.join(', ')} WHERE id = ?`).run(...args);
216
+ await dbRun(`UPDATE secondhand_items SET ${sets.join(', ')} WHERE id = ?`, args);
214
217
  res.json({ success: true });
215
218
  });
216
- // 6. 下单(CAS 锁库存)
219
+ // 6. 下单(CAS 锁库存)— money/escrow + pragma FK-OFF 窗口,保持同步,Phase 3 随资金路径迁移
217
220
  app.post('/api/secondhand/:id/order', (req, res) => {
218
221
  const user = auth(req, res);
219
222
  if (!user)
@@ -1,7 +1,9 @@
1
+ import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
1
2
  export function registerSellerQuotaRoutes(app, deps) {
2
- const { db, generateId, auth, requireUsersAdmin, safeRoles, checkSellerCanList, adminCanOperateOn, logAdminAction, QUOTA_TIERS } = deps;
3
+ // db 已走 RFC-016 异步 seam(dbOne/dbAll/dbRun),不再直接用 deps.db
4
+ const { generateId, auth, requireUsersAdmin, safeRoles, checkSellerCanList, adminCanOperateOn, logAdminAction, QUOTA_TIERS } = deps;
3
5
  // 配额状态
4
- app.get('/api/seller/quota-status', (req, res) => {
6
+ app.get('/api/seller/quota-status', async (req, res) => {
5
7
  const user = auth(req, res);
6
8
  if (!user)
7
9
  return;
@@ -9,7 +11,7 @@ export function registerSellerQuotaRoutes(app, deps) {
9
11
  return void res.json({ error: '仅卖家可查看配额' });
10
12
  }
11
13
  const status = checkSellerCanList(user);
12
- const pending = db.prepare("SELECT id, requested_quota, applied_at FROM quota_increase_applications WHERE user_id = ? AND status = 'pending' ORDER BY applied_at DESC LIMIT 1").get(user.id);
14
+ const pending = await dbOne("SELECT id, requested_quota, applied_at FROM quota_increase_applications WHERE user_id = ? AND status = 'pending' ORDER BY applied_at DESC LIMIT 1", [user.id]);
13
15
  const max = Number(user.max_products ?? 200);
14
16
  const nextTierIdx = QUOTA_TIERS.indexOf(max);
15
17
  const nextTier = nextTierIdx >= 0 && nextTierIdx < QUOTA_TIERS.length - 1 ? QUOTA_TIERS[nextTierIdx + 1] : null;
@@ -28,7 +30,7 @@ export function registerSellerQuotaRoutes(app, deps) {
28
30
  });
29
31
  });
30
32
  // 数据中心(30d GMV / 7d 曲线 / Top 5 / 客户洞察 / 状态分布)
31
- app.get('/api/seller/insights', (req, res) => {
33
+ app.get('/api/seller/insights', async (req, res) => {
32
34
  const user = auth(req, res);
33
35
  if (!user)
34
36
  return;
@@ -42,7 +44,7 @@ export function registerSellerQuotaRoutes(app, deps) {
42
44
  const fmt = (d) => d.toISOString().replace('T', ' ').slice(0, 19);
43
45
  const d30 = fmt(new Date(now - 30 * 86400000));
44
46
  const d60 = fmt(new Date(now - 60 * 86400000));
45
- const orders = db.prepare(`
47
+ const orders = await dbAll(`
46
48
  SELECT o.id, o.product_id, o.buyer_id, o.status, o.total_amount, o.created_at,
47
49
  COALESCE(p.title, '已下架商品') as product_title,
48
50
  COALESCE(ub.name, '匿名') as buyer_name
@@ -51,7 +53,7 @@ export function registerSellerQuotaRoutes(app, deps) {
51
53
  LEFT JOIN users ub ON o.buyer_id = ub.id
52
54
  WHERE o.seller_id = ? AND o.created_at >= ?
53
55
  ORDER BY o.created_at DESC
54
- `).all(sellerId, d60);
56
+ `, [sellerId, d60]);
55
57
  const completedStatuses = new Set(['completed', 'confirmed']);
56
58
  const disputedStatuses = new Set(['disputed', 'fault_seller', 'fault_buyer', 'fault_logistics', 'resolved_for_seller', 'refunded_partial', 'refunded_full', 'dispute_dismissed']);
57
59
  const cancelledStatuses = new Set(['cancelled', 'expired']);
@@ -135,7 +137,7 @@ export function registerSellerQuotaRoutes(app, deps) {
135
137
  status_breakdown: statusBreakdown,
136
138
  });
137
139
  });
138
- app.post('/api/seller/apply-quota-increase', (req, res) => {
140
+ app.post('/api/seller/apply-quota-increase', async (req, res) => {
139
141
  const user = auth(req, res);
140
142
  if (!user)
141
143
  return;
@@ -152,45 +154,43 @@ export function registerSellerQuotaRoutes(app, deps) {
152
154
  if (Number(requested_quota) !== nextTier) {
153
155
  return void res.json({ error: `下一档配额应为 ${nextTier}` });
154
156
  }
155
- const existing = db.prepare("SELECT 1 FROM quota_increase_applications WHERE user_id = ? AND status = 'pending' LIMIT 1").get(user.id);
157
+ const existing = await dbOne("SELECT 1 FROM quota_increase_applications WHERE user_id = ? AND status = 'pending' LIMIT 1", [user.id]);
156
158
  if (existing)
157
159
  return void res.json({ error: '已有待审申请' });
158
- db.prepare(`INSERT INTO quota_increase_applications (id, user_id, current_quota, requested_quota, reason) VALUES (?,?,?,?,?)`)
159
- .run(generateId('qapp'), user.id, current, nextTier, (reason || '').toString().slice(0, 500));
160
+ await dbRun(`INSERT INTO quota_increase_applications (id, user_id, current_quota, requested_quota, reason) VALUES (?,?,?,?,?)`, [generateId('qapp'), user.id, current, nextTier, (reason || '').toString().slice(0, 500)]);
160
161
  res.json({ success: true, requested_quota: nextTier });
161
162
  });
162
- app.post('/api/seller/withdraw-quota-application', (req, res) => {
163
+ app.post('/api/seller/withdraw-quota-application', async (req, res) => {
163
164
  const user = auth(req, res);
164
165
  if (!user)
165
166
  return;
166
- const pending = db.prepare("SELECT id FROM quota_increase_applications WHERE user_id = ? AND status = 'pending' LIMIT 1").get(user.id);
167
+ const pending = await dbOne("SELECT id FROM quota_increase_applications WHERE user_id = ? AND status = 'pending' LIMIT 1", [user.id]);
167
168
  if (!pending)
168
169
  return void res.json({ error: '没有待审申请' });
169
- db.prepare("UPDATE quota_increase_applications SET status = 'withdrawn', reviewed_at = datetime('now') WHERE id = ?").run(pending.id);
170
+ await dbRun("UPDATE quota_increase_applications SET status = 'withdrawn', reviewed_at = datetime('now') WHERE id = ?", [pending.id]);
170
171
  res.json({ success: true });
171
172
  });
172
173
  // Admin
173
- app.get('/api/admin/quota-applications', (req, res) => {
174
+ app.get('/api/admin/quota-applications', async (req, res) => {
174
175
  const admin = requireUsersAdmin(req, res);
175
176
  if (!admin)
176
177
  return;
177
178
  const status = req.query.status || 'pending';
178
- const rows = db.prepare(`
179
+ const rows = await dbAll(`
179
180
  SELECT qa.*, u.name as user_name, u.email
180
181
  FROM quota_increase_applications qa
181
182
  LEFT JOIN users u ON u.id = qa.user_id
182
183
  WHERE qa.status = ?
183
184
  ORDER BY qa.applied_at DESC LIMIT 100
184
- `).all(status);
185
+ `, [status]);
185
186
  res.json({ applications: rows });
186
187
  });
187
- app.post('/api/admin/quota-applications/:id/approve', (req, res) => {
188
+ app.post('/api/admin/quota-applications/:id/approve', async (req, res) => {
188
189
  const admin = requireUsersAdmin(req, res);
189
190
  if (!admin)
190
191
  return;
191
192
  const { note } = req.body;
192
- const appRow = db.prepare("SELECT id, user_id, requested_quota, status FROM quota_increase_applications WHERE id = ?")
193
- .get(req.params.id);
193
+ const appRow = await dbOne("SELECT id, user_id, requested_quota, status FROM quota_increase_applications WHERE id = ?", [req.params.id]);
194
194
  if (!appRow)
195
195
  return void res.json({ error: '申请不存在' });
196
196
  if (!adminCanOperateOn(admin, appRow.user_id, res))
@@ -199,26 +199,24 @@ export function registerSellerQuotaRoutes(app, deps) {
199
199
  return void res.json({ error: '该申请不在待审状态' });
200
200
  if (!QUOTA_TIERS.includes(appRow.requested_quota))
201
201
  return void res.json({ error: '请求配额不合法' });
202
- db.prepare("UPDATE quota_increase_applications SET status='approved', reviewed_at=datetime('now'), reviewed_by=?, decision_note=? WHERE id=?")
203
- .run(admin.id, note || null, appRow.id);
204
- db.prepare("UPDATE users SET max_products = ?, updated_at = datetime('now') WHERE id = ?").run(appRow.requested_quota, appRow.user_id);
202
+ await dbRun("UPDATE quota_increase_applications SET status='approved', reviewed_at=datetime('now'), reviewed_by=?, decision_note=? WHERE id=?", [admin.id, note || null, appRow.id]);
203
+ await dbRun("UPDATE users SET max_products = ?, updated_at = datetime('now') WHERE id = ?", [appRow.requested_quota, appRow.user_id]);
205
204
  logAdminAction(admin.id, 'approve_quota_increase', 'user', appRow.user_id, { quota: appRow.requested_quota, note });
206
205
  res.json({ success: true });
207
206
  });
208
- app.post('/api/admin/quota-applications/:id/reject', (req, res) => {
207
+ app.post('/api/admin/quota-applications/:id/reject', async (req, res) => {
209
208
  const admin = requireUsersAdmin(req, res);
210
209
  if (!admin)
211
210
  return;
212
211
  const { note } = req.body;
213
- const appRow = db.prepare("SELECT id, user_id, status FROM quota_increase_applications WHERE id = ?").get(req.params.id);
212
+ const appRow = await dbOne("SELECT id, user_id, status FROM quota_increase_applications WHERE id = ?", [req.params.id]);
214
213
  if (!appRow)
215
214
  return void res.json({ error: '申请不存在' });
216
215
  if (!adminCanOperateOn(admin, appRow.user_id, res))
217
216
  return;
218
217
  if (appRow.status !== 'pending')
219
218
  return void res.json({ error: '该申请不在待审状态' });
220
- db.prepare("UPDATE quota_increase_applications SET status='rejected', reviewed_at=datetime('now'), reviewed_by=?, decision_note=? WHERE id=?")
221
- .run(admin.id, note || null, appRow.id);
219
+ await dbRun("UPDATE quota_increase_applications SET status='rejected', reviewed_at=datetime('now'), reviewed_by=?, decision_note=? WHERE id=?", [admin.id, note || null, appRow.id]);
222
220
  logAdminAction(admin.id, 'reject_quota_increase', 'user', appRow.user_id, { note });
223
221
  res.json({ success: true });
224
222
  });
@@ -1,7 +1,29 @@
1
1
  import QRCode from 'qrcode';
2
2
  import { createHash } from 'crypto';
3
+ import { dbOne, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
3
4
  export function registerShareRedirectsRoutes(app, deps) {
4
- const { db, auth, clientIpHash, clientUaHash } = deps;
5
+ // db 已全量走 RFC-016 异步 seam(dbOne/dbRun),不再直接用 deps.db
6
+ const { auth, clientIpHash, clientUaHash, resolveInviteCodeRef } = deps;
7
+ // Resolve the ?ref for a /s/<shareable> redirect: a permanent_code ONLY — never the raw owner_id (usr_xxx).
8
+ // Prefer the shareable's stored owner_code; if missing/invalid, look up the owner's permanent_code and
9
+ // opportunistically backfill owner_code. If the owner has no permanent_code, return null (emit NO ref).
10
+ async function shareOwnerRefCode(ownerCode, ownerId) {
11
+ const oc = typeof ownerCode === 'string' ? ownerCode.trim().toUpperCase() : '';
12
+ if (/^[A-Z0-9]{6,7}$/.test(oc))
13
+ return oc;
14
+ const oid = typeof ownerId === 'string' ? ownerId : '';
15
+ if (oid) {
16
+ const r = await dbOne("SELECT permanent_code FROM users WHERE id = ? AND id != 'sys_protocol'", [oid]);
17
+ if (r?.permanent_code) {
18
+ try {
19
+ await dbRun("UPDATE shareables SET owner_code = ? WHERE owner_id = ? AND (owner_code IS NULL OR owner_code = '')", [r.permanent_code, oid]);
20
+ }
21
+ catch { }
22
+ return r.permanent_code;
23
+ }
24
+ }
25
+ return null;
26
+ }
5
27
  // 二维码生成(24h cache + ETag)
6
28
  app.get('/api/qr', async (req, res) => {
7
29
  const text = String(req.query.text || '').slice(0, 1024).trim();
@@ -24,27 +46,27 @@ export function registerShareRedirectsRoutes(app, deps) {
24
46
  });
25
47
  // 商品分享短链 /s/<shareable_id>
26
48
  // 笔记 → 跳 PWA 内 #note/<id>;商品 → #order-product/<id>;外链 → 直跳外部 URL
27
- app.get('/s/:id', (req, res) => {
49
+ app.get('/s/:id', async (req, res) => {
28
50
  const id = String(req.params.id || '').trim();
29
- const row = db.prepare(`
51
+ const row = await dbOne(`
30
52
  SELECT id, owner_id, owner_code, type, external_url, related_product_id, related_anchor, status
31
53
  FROM shareables WHERE id = ? AND status = 'active'
32
- `).get(id);
54
+ `, [id]);
33
55
  // Phase C 笔记着陆页 — type=note 优先跳 PWA 内的 #note/<id>
34
56
  if (row && row.type === 'note') {
35
- const ownerRef = row.owner_code || row.owner_id || '';
57
+ const ownerRef = await shareOwnerRefCode(row.owner_code, row.owner_id);
36
58
  const qs = new URLSearchParams();
37
59
  if (ownerRef)
38
- qs.set('ref', String(ownerRef));
60
+ qs.set('ref', ownerRef);
39
61
  qs.set('share_id', id);
40
62
  try {
41
63
  const ipHash = clientIpHash(req);
42
64
  const uaHash = clientUaHash(req);
43
- const dup = db.prepare(`SELECT 1 FROM shareable_click_log WHERE shareable_id = ? AND ip_hash = ? AND ua_hash = ? AND created_at > datetime('now', '-6 hours') LIMIT 1`).get(id, ipHash, uaHash);
44
- db.prepare(`INSERT INTO shareable_click_log (shareable_id, ip_hash, ua_hash, ref_path) VALUES (?,?,?,?)`).run(id, ipHash, uaHash, req.originalUrl || null);
45
- db.prepare(`UPDATE shareables SET click_count = COALESCE(click_count,0) + 1 WHERE id = ?`).run(id);
65
+ const dup = await dbOne(`SELECT 1 FROM shareable_click_log WHERE shareable_id = ? AND ip_hash = ? AND ua_hash = ? AND created_at > datetime('now', '-6 hours') LIMIT 1`, [id, ipHash, uaHash]);
66
+ await dbRun(`INSERT INTO shareable_click_log (shareable_id, ip_hash, ua_hash, ref_path) VALUES (?,?,?,?)`, [id, ipHash, uaHash, req.originalUrl || null]);
67
+ await dbRun(`UPDATE shareables SET click_count = COALESCE(click_count,0) + 1 WHERE id = ?`, [id]);
46
68
  if (!dup)
47
- db.prepare(`UPDATE shareables SET unique_click_count = COALESCE(unique_click_count,0) + 1 WHERE id = ?`).run(id);
69
+ await dbRun(`UPDATE shareables SET unique_click_count = COALESCE(unique_click_count,0) + 1 WHERE id = ?`, [id]);
48
70
  }
49
71
  catch (e) {
50
72
  console.error('[note-click]', e);
@@ -57,26 +79,25 @@ export function registerShareRedirectsRoutes(app, deps) {
57
79
  try {
58
80
  const ipHash = clientIpHash(req);
59
81
  const uaHash = clientUaHash(req);
60
- const dup = db.prepare(`
82
+ const dup = await dbOne(`
61
83
  SELECT 1 FROM shareable_click_log
62
84
  WHERE shareable_id = ? AND ip_hash = ? AND ua_hash = ?
63
85
  AND created_at > datetime('now', '-6 hours') LIMIT 1
64
- `).get(id, ipHash, uaHash);
65
- db.prepare(`INSERT INTO shareable_click_log (shareable_id, ip_hash, ua_hash, ref_path) VALUES (?,?,?,?)`)
66
- .run(id, ipHash, uaHash, req.originalUrl || null);
67
- db.prepare(`UPDATE shareables SET click_count = COALESCE(click_count,0) + 1 WHERE id = ?`).run(id);
86
+ `, [id, ipHash, uaHash]);
87
+ await dbRun(`INSERT INTO shareable_click_log (shareable_id, ip_hash, ua_hash, ref_path) VALUES (?,?,?,?)`, [id, ipHash, uaHash, req.originalUrl || null]);
88
+ await dbRun(`UPDATE shareables SET click_count = COALESCE(click_count,0) + 1 WHERE id = ?`, [id]);
68
89
  if (!dup) {
69
- db.prepare(`UPDATE shareables SET unique_click_count = COALESCE(unique_click_count,0) + 1 WHERE id = ?`).run(id);
90
+ await dbRun(`UPDATE shareables SET unique_click_count = COALESCE(unique_click_count,0) + 1 WHERE id = ?`, [id]);
70
91
  }
71
92
  }
72
93
  catch (e) {
73
94
  console.error('[M3-click]', e);
74
95
  }
75
- const ownerRef = row.owner_code || row.owner_id || '';
96
+ const ownerRef = await shareOwnerRefCode(row.owner_code, row.owner_id);
76
97
  const isProduct = !!row.related_product_id;
77
98
  const baseParams = new URLSearchParams();
78
99
  if (ownerRef)
79
- baseParams.set('ref', String(ownerRef));
100
+ baseParams.set('ref', ownerRef);
80
101
  if (isProduct)
81
102
  baseParams.set('share_id', id);
82
103
  const qs = baseParams.toString() ? '?' + baseParams.toString() : '';
@@ -92,7 +113,7 @@ export function registerShareRedirectsRoutes(app, deps) {
92
113
  res.redirect(302, `/${qs}#u/${row.owner_id}`);
93
114
  });
94
115
  // 商品分享归因落库(前端登录后首次进入带 share_id 时调用)
95
- app.post('/api/product-share/touch', (req, res) => {
116
+ app.post('/api/product-share/touch', async (req, res) => {
96
117
  const user = auth(req, res);
97
118
  if (!user)
98
119
  return;
@@ -100,25 +121,25 @@ export function registerShareRedirectsRoutes(app, deps) {
100
121
  if (!shareable_id || typeof shareable_id !== 'string') {
101
122
  return void res.json({ ok: false, error: 'invalid_shareable_id' });
102
123
  }
103
- const s = db.prepare(`
124
+ const s = await dbOne(`
104
125
  SELECT id, owner_id, related_product_id FROM shareables
105
126
  WHERE id = ? AND status = 'active'
106
- `).get(shareable_id);
127
+ `, [shareable_id]);
107
128
  if (!s || !s.related_product_id)
108
129
  return void res.json({ ok: false, error: 'not_product_shareable' });
109
130
  if (s.owner_id === user.id)
110
131
  return void res.json({ ok: false, error: 'cannot_self_attribute' });
111
132
  const expiresAt = new Date(Date.now() + 30 * 86400_000).toISOString().slice(0, 19).replace('T', ' ');
112
133
  // first-touch:未过期 → 静默保留;过期 → 替换
113
- const existing = db.prepare(`
134
+ const existing = await dbOne(`
114
135
  SELECT sharer_id, expires_at FROM product_share_attribution
115
136
  WHERE product_id = ? AND recipient_id = ?
116
- `).get(s.related_product_id, user.id);
137
+ `, [s.related_product_id, user.id]);
117
138
  if (!existing) {
118
- db.prepare(`
119
- INSERT INTO product_share_attribution (product_id, recipient_id, sharer_id, shareable_id, expires_at)
120
- VALUES (?, ?, ?, ?, ?)
121
- `).run(s.related_product_id, user.id, s.owner_id, s.id, expiresAt);
139
+ await dbRun(`
140
+ INSERT INTO product_share_attribution (product_id, recipient_id, sharer_id, shareable_id, expires_at, source_type, source_ref)
141
+ VALUES (?, ?, ?, ?, ?, 'direct_share', ?)
142
+ `, [s.related_product_id, user.id, s.owner_id, s.id, expiresAt, s.id]);
122
143
  return void res.json({ ok: true, attributed: true, sharer_id: s.owner_id, product_id: s.related_product_id });
123
144
  }
124
145
  const stillValid = new Date(existing.expires_at.replace(' ', 'T') + 'Z').getTime() > Date.now();
@@ -126,39 +147,23 @@ export function registerShareRedirectsRoutes(app, deps) {
126
147
  return void res.json({ ok: true, attributed: false, existing_sharer_id: existing.sharer_id, locked: true });
127
148
  }
128
149
  // 已过期 → 刷新
129
- db.prepare(`
150
+ await dbRun(`
130
151
  UPDATE product_share_attribution
131
- SET sharer_id = ?, shareable_id = ?, created_at = datetime('now'), expires_at = ?
152
+ SET sharer_id = ?, shareable_id = ?, created_at = datetime('now'), expires_at = ?,
153
+ source_type = 'direct_share', source_ref = ?, source_shop_seller_id = NULL, source_qualified_order_id = NULL
132
154
  WHERE product_id = ? AND recipient_id = ?
133
- `).run(s.owner_id, s.id, expiresAt, s.related_product_id, user.id);
155
+ `, [s.owner_id, s.id, expiresAt, s.id, s.related_product_id, user.id]);
134
156
  res.json({ ok: true, attributed: true, refreshed: true, sharer_id: s.owner_id });
135
157
  });
136
- // 邀请短链 /i/CODE
137
- // 格式:/i/VKSF9P / /i/VKSF9P-L / /i/VKSF9P-R / /i/@handle / /i/@handle-L
158
+ // 邀请短链 /i/CODE — invite-code ONLY (permanent_code, 兼容旧的 -L/-R 后缀). usr_xxx / @handle / 裸 handle
159
+ // 一律 404(不再做 handle 解析)。pre-public 去左右码:/i/CODE 与旧 /i/CODE-L、/i/CODE-R 一律
160
+ // 规范化重定向到 /?ref=CODE(丢弃 side;放置侧别由注册时系统自动决定),旧链接/二维码仍可用。
161
+ // 不受 invite_rotation_enabled 影响:已有用户分享出的 /i/CODE 链接和二维码必须始终可用。
138
162
  app.get('/i/:code', (req, res) => {
139
- let raw = String(req.params.code || '').trim();
140
- let side = null;
141
- const m = raw.match(/^(.+?)-([lLrR])$/);
142
- if (m) {
143
- raw = m[1];
144
- side = m[2].toLowerCase() === 'l' ? 'left' : 'right';
145
- }
146
- // permanent_code 或 handle 规范化
147
- let display = null;
148
- if (/^[A-Z0-9]{6,7}$/.test(raw.toUpperCase()) && !raw.startsWith('@')) {
149
- const r = db.prepare("SELECT permanent_code FROM users WHERE permanent_code = ? AND id != 'sys_protocol'").get(raw.toUpperCase());
150
- if (r)
151
- display = r.permanent_code;
152
- }
153
- if (!display) {
154
- const h = raw.replace(/^@/, '').toLowerCase();
155
- const r = db.prepare("SELECT handle FROM users WHERE handle = ? AND id != 'sys_protocol'").get(h);
156
- if (r)
157
- display = '@' + r.handle; // @ 前缀辨识 handle 形态
158
- }
159
- if (!display)
160
- return void res.status(404).send('Invitation link not found. Code: ' + raw);
161
- const target = `/?ref=${encodeURIComponent(display)}${side ? `&side=${side}` : ''}`;
163
+ const ref = resolveInviteCodeRef(String(req.params.code || ''));
164
+ if (!ref)
165
+ return void res.status(404).send('Invitation link not found.');
166
+ const target = `/?ref=${encodeURIComponent(ref.code)}`;
162
167
  res.redirect(302, target);
163
168
  });
164
169
  }