@seasonkoh/webaz 0.1.23 → 0.1.25

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 (187) hide show
  1. package/README.md +2 -0
  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 +198 -83
  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 +173 -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 +10 -2
  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-engine.js +109 -0
  35. package/dist/layer2-business/L2-9-contribution/identity-claim-fact-precondition.js +22 -0
  36. package/dist/layer2-business/L2-9-contribution/identity-claim-proof-verifier.js +97 -0
  37. package/dist/layer2-business/L2-9-contribution/identity-claim-read.js +59 -0
  38. package/dist/layer2-business/L2-9-contribution/task-proposal-store.js +129 -0
  39. package/dist/layer2-business/L2-notes/note-photo-storage.js +4 -2
  40. package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +17 -15
  41. package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +11 -8
  42. package/dist/layer4-economics/L4-3-reputation/reputation-engine.js +9 -8
  43. package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +11 -8
  44. package/dist/layer4-economics/L4-4-skill-market/skill-listing-engine.js +22 -16
  45. package/dist/pwa/acp-feed.js +13 -1
  46. package/dist/pwa/contract-fingerprint.js +2 -0
  47. package/dist/pwa/endpoint-actions.js +5 -1
  48. package/dist/pwa/goal-index.js +8 -8
  49. package/dist/pwa/human-presence.js +62 -0
  50. package/dist/pwa/public/app.js +575 -68
  51. package/dist/pwa/public/i18n.js +29 -20
  52. package/dist/pwa/public/index.html +1 -0
  53. package/dist/pwa/public/openapi.json +2 -2
  54. package/dist/pwa/rate-limit.js +22 -0
  55. package/dist/pwa/routes/account-deletion.js +15 -13
  56. package/dist/pwa/routes/addresses.js +10 -9
  57. package/dist/pwa/routes/admin-admins.js +13 -14
  58. package/dist/pwa/routes/admin-analytics.js +109 -69
  59. package/dist/pwa/routes/admin-catalog.js +13 -11
  60. package/dist/pwa/routes/admin-editor-picks.js +15 -10
  61. package/dist/pwa/routes/admin-events.js +5 -3
  62. package/dist/pwa/routes/admin-health.js +2 -1
  63. package/dist/pwa/routes/admin-moderation.js +26 -29
  64. package/dist/pwa/routes/admin-ops.js +22 -21
  65. package/dist/pwa/routes/admin-protocol-params.js +16 -19
  66. package/dist/pwa/routes/admin-reports.js +23 -21
  67. package/dist/pwa/routes/admin-tokenomics.js +26 -25
  68. package/dist/pwa/routes/admin-users-lifecycle.js +37 -40
  69. package/dist/pwa/routes/admin-users-query.js +54 -53
  70. package/dist/pwa/routes/admin-verifier-flow.js +82 -41
  71. package/dist/pwa/routes/admin-verifier-whitelist.js +55 -27
  72. package/dist/pwa/routes/admin-wallet-ops.js +7 -5
  73. package/dist/pwa/routes/agent-buy.js +46 -22
  74. package/dist/pwa/routes/agent-governance.js +52 -56
  75. package/dist/pwa/routes/ai.js +7 -5
  76. package/dist/pwa/routes/analytics.js +43 -41
  77. package/dist/pwa/routes/anchors.js +19 -20
  78. package/dist/pwa/routes/announcements.js +13 -13
  79. package/dist/pwa/routes/arbitrator.js +97 -31
  80. package/dist/pwa/routes/auction.js +153 -114
  81. package/dist/pwa/routes/auth-login.js +6 -4
  82. package/dist/pwa/routes/auth-read.js +11 -9
  83. package/dist/pwa/routes/auth-register.js +35 -20
  84. package/dist/pwa/routes/auth-sessions.js +12 -11
  85. package/dist/pwa/routes/blocklist.js +16 -15
  86. package/dist/pwa/routes/build-feedback.js +10 -9
  87. package/dist/pwa/routes/build-reputation.js +6 -2
  88. package/dist/pwa/routes/build-tasks.js +45 -13
  89. package/dist/pwa/routes/buyer-feeds.js +27 -25
  90. package/dist/pwa/routes/cart.js +16 -15
  91. package/dist/pwa/routes/charity.js +212 -150
  92. package/dist/pwa/routes/chat.js +42 -43
  93. package/dist/pwa/routes/checkin-tasks.js +10 -9
  94. package/dist/pwa/routes/checkout-helpers.js +12 -10
  95. package/dist/pwa/routes/claim-initiators.js +34 -14
  96. package/dist/pwa/routes/claim-verify.js +86 -53
  97. package/dist/pwa/routes/claim-voting.js +43 -18
  98. package/dist/pwa/routes/contribution-identity.js +147 -0
  99. package/dist/pwa/routes/contribution-score.js +19 -0
  100. package/dist/pwa/routes/coupons.js +19 -16
  101. package/dist/pwa/routes/dashboards.js +18 -16
  102. package/dist/pwa/routes/dispute-cases.js +25 -24
  103. package/dist/pwa/routes/disputes-read.js +45 -51
  104. package/dist/pwa/routes/disputes-write.js +124 -61
  105. package/dist/pwa/routes/evidence.js +9 -9
  106. package/dist/pwa/routes/external-anchors.js +13 -12
  107. package/dist/pwa/routes/feedback.js +29 -33
  108. package/dist/pwa/routes/flash-sales.js +18 -16
  109. package/dist/pwa/routes/follows.js +25 -24
  110. package/dist/pwa/routes/governance-auto-deactivate.js +21 -9
  111. package/dist/pwa/routes/governance-onboarding.js +70 -59
  112. package/dist/pwa/routes/group-buys.js +22 -22
  113. package/dist/pwa/routes/growth.js +33 -30
  114. package/dist/pwa/routes/import-product.js +12 -10
  115. package/dist/pwa/routes/kyc.js +9 -8
  116. package/dist/pwa/routes/leaderboard.js +20 -18
  117. package/dist/pwa/routes/listings.js +23 -22
  118. package/dist/pwa/routes/logistics.js +10 -8
  119. package/dist/pwa/routes/manifests.js +27 -27
  120. package/dist/pwa/routes/me-data.js +23 -21
  121. package/dist/pwa/routes/notifications.js +7 -6
  122. package/dist/pwa/routes/offers.js +30 -12
  123. package/dist/pwa/routes/orders-action.js +33 -17
  124. package/dist/pwa/routes/orders-create.js +75 -20
  125. package/dist/pwa/routes/orders-read.js +21 -20
  126. package/dist/pwa/routes/p2p-products.js +30 -18
  127. package/dist/pwa/routes/payments-governance.js +61 -56
  128. package/dist/pwa/routes/peers.js +9 -8
  129. package/dist/pwa/routes/pin-receipts.js +13 -13
  130. package/dist/pwa/routes/products-aliases.js +12 -10
  131. package/dist/pwa/routes/products-claims.js +36 -17
  132. package/dist/pwa/routes/products-create.js +53 -38
  133. package/dist/pwa/routes/products-crud.js +17 -16
  134. package/dist/pwa/routes/products-links.js +49 -26
  135. package/dist/pwa/routes/products-list.js +6 -4
  136. package/dist/pwa/routes/products-meta.js +40 -39
  137. package/dist/pwa/routes/products-update.js +19 -5
  138. package/dist/pwa/routes/profile-credentials.js +14 -16
  139. package/dist/pwa/routes/profile-identity.js +14 -13
  140. package/dist/pwa/routes/profile-location.js +7 -6
  141. package/dist/pwa/routes/profile-placement.js +19 -17
  142. package/dist/pwa/routes/profile-prefs.js +11 -11
  143. package/dist/pwa/routes/promoter.js +55 -49
  144. package/dist/pwa/routes/public-build-tasks.js +19 -0
  145. package/dist/pwa/routes/public-utils.js +108 -46
  146. package/dist/pwa/routes/push.js +16 -15
  147. package/dist/pwa/routes/ratings.js +30 -30
  148. package/dist/pwa/routes/recover-key.js +13 -12
  149. package/dist/pwa/routes/referral.js +37 -32
  150. package/dist/pwa/routes/reputation.js +3 -2
  151. package/dist/pwa/routes/returns.js +76 -73
  152. package/dist/pwa/routes/reviews.js +41 -18
  153. package/dist/pwa/routes/rewards-apply.js +16 -15
  154. package/dist/pwa/routes/rewards-auto-downgrade.js +9 -7
  155. package/dist/pwa/routes/rewards-escrow-expire.js +7 -5
  156. package/dist/pwa/routes/rfqs.js +163 -85
  157. package/dist/pwa/routes/search.js +16 -14
  158. package/dist/pwa/routes/secondhand.js +25 -22
  159. package/dist/pwa/routes/seller-quota.js +24 -26
  160. package/dist/pwa/routes/share-redirects.js +59 -55
  161. package/dist/pwa/routes/shareables-interactions.js +34 -35
  162. package/dist/pwa/routes/shareables.js +55 -51
  163. package/dist/pwa/routes/shop-referral.js +57 -0
  164. package/dist/pwa/routes/shops.js +20 -18
  165. package/dist/pwa/routes/signaling.js +10 -9
  166. package/dist/pwa/routes/skill-market.js +16 -16
  167. package/dist/pwa/routes/skills.js +15 -14
  168. package/dist/pwa/routes/snf.js +14 -13
  169. package/dist/pwa/routes/tags.js +10 -9
  170. package/dist/pwa/routes/task-proposals.js +45 -0
  171. package/dist/pwa/routes/trial.js +69 -51
  172. package/dist/pwa/routes/trusted-kpi.js +20 -18
  173. package/dist/pwa/routes/url-claim.js +67 -28
  174. package/dist/pwa/routes/users-public.js +62 -60
  175. package/dist/pwa/routes/variants.js +12 -13
  176. package/dist/pwa/routes/verifier-user.js +61 -21
  177. package/dist/pwa/routes/verify-tasks.js +49 -25
  178. package/dist/pwa/routes/waitlist.js +16 -15
  179. package/dist/pwa/routes/wallet-read.js +74 -36
  180. package/dist/pwa/routes/wallet-write.js +12 -9
  181. package/dist/pwa/routes/webauthn.js +25 -26
  182. package/dist/pwa/routes/webhooks.js +26 -26
  183. package/dist/pwa/routes/welcome.js +45 -50
  184. package/dist/pwa/routes/wishlist-qa.js +29 -32
  185. package/dist/pwa/server.js +237 -81
  186. package/dist/version.js +1 -1
  187. package/package.json +47 -2
@@ -1,6 +1,7 @@
1
+ import { dbOne, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
1
2
  export function registerProductsCreateRoutes(app, deps) {
2
3
  const { db, auth, generateId, checkSellerCanList, getStakeDiscount, VALID_PRODUCT_TYPES, parsePlatformUrl, makeCommitmentHash, makeDescriptionHash, makePriceHash } = deps;
3
- app.post('/api/products', (req, res) => {
4
+ app.post('/api/products', async (req, res) => {
4
5
  const user = auth(req, res);
5
6
  if (!user)
6
7
  return;
@@ -8,10 +9,10 @@ export function registerProductsCreateRoutes(app, deps) {
8
9
  return void res.json({ error: '仅卖家可上架商品' });
9
10
  // 里程碑 3-D:1h 上架限速(防 spam 批量上架)
10
11
  const LISTING_RATE_LIMIT = 5;
11
- const recentListings = db.prepare(`
12
+ const recentListings = (await dbOne(`
12
13
  SELECT COUNT(1) as n FROM products
13
14
  WHERE seller_id = ? AND created_at > datetime('now', '-1 hour')
14
- `).get(user.id).n;
15
+ `, [user.id])).n;
15
16
  if (recentListings >= LISTING_RATE_LIMIT) {
16
17
  return void res.status(429).json({
17
18
  error: `上架过于频繁:1 小时内最多 ${LISTING_RATE_LIMIT} 个商品(当前已 ${recentListings} 个)`,
@@ -54,11 +55,11 @@ export function registerProductsCreateRoutes(app, deps) {
54
55
  }
55
56
  // 上架前检查:同一卖家不能重复关联相同外部链接
56
57
  if (source_url) {
57
- const sameSellerDupe = db.prepare(`
58
+ const sameSellerDupe = (await dbOne(`
58
59
  SELECT COUNT(*) as n FROM product_external_links pel
59
60
  JOIN products p ON pel.product_id = p.id
60
61
  WHERE pel.url = ? AND p.seller_id = ?
61
- `).get(source_url, user.id);
62
+ `, [source_url, user.id]));
62
63
  if (sameSellerDupe.n > 0) {
63
64
  return void res.json({ error: '您已上架过来自此链接的商品,不能重复关联相同外部链接' });
64
65
  }
@@ -66,7 +67,7 @@ export function registerProductsCreateRoutes(app, deps) {
66
67
  // M7.2.6 / 方案 3:上架免质押 — 零门槛入驻
67
68
  // stake_amount 字段记录"预期 stake"(首单成交时从订单 escrow 锁定)
68
69
  const priceNum = Number(price);
69
- const stakeDiscount = getStakeDiscount(db, user.id);
70
+ const stakeDiscount = await getStakeDiscount(db, user.id);
70
71
  const stakeRate = Math.max(0.05, 0.15 - stakeDiscount);
71
72
  const stakeAmount = Math.round(priceNum * stakeRate * 100) / 100;
72
73
  const now = new Date().toISOString();
@@ -74,14 +75,23 @@ export function registerProductsCreateRoutes(app, deps) {
74
75
  const specsJson = specs ? (typeof specs === 'string' ? specs : JSON.stringify(specs)) : null;
75
76
  const estJson = estimated_days ? (typeof estimated_days === 'string' ? estimated_days : JSON.stringify(estimated_days)) : null;
76
77
  const pFields = { ship_regions, handling_hours, estimated_days: estJson, return_days, return_condition, warranty_days };
77
- db.prepare(`INSERT INTO products (
78
+ await dbRun(`INSERT INTO products (
78
79
  id, seller_id, title, description, price, stock, category, stake_amount,
79
80
  specs, brand, model, source_url, source_price, source_price_at,
80
81
  weight_kg, ship_regions, handling_hours, estimated_days, fragile,
81
82
  return_days, return_condition, warranty_days,
82
83
  commitment_hash, description_hash, price_hash, hashed_at,
83
84
  commission_rate, product_type, images
84
- ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(id, user.id, title, description, priceNum, Number(stock), category, stakeAmount, specsJson, brand ?? null, model ?? null, source_url ?? null, source_price ? Number(source_price) : null, source_price ? now : null, weight_kg ? Number(weight_kg) : null, ship_regions, Number(handling_hours), estJson, fragile ? 1 : 0, Number(return_days), return_condition, Number(warranty_days), makeCommitmentHash(pFields), makeDescriptionHash({ title, description, specs: specsJson }), makePriceHash(priceNum, now), now, commissionRateNum, product_type, imagesJsonForInsert);
85
+ ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`, [
86
+ id, user.id, title, description, priceNum, Number(stock), category, stakeAmount,
87
+ specsJson, brand ?? null, model ?? null,
88
+ source_url ?? null, source_price ? Number(source_price) : null, source_price ? now : null,
89
+ weight_kg ? Number(weight_kg) : null, ship_regions, Number(handling_hours), estJson, fragile ? 1 : 0,
90
+ Number(return_days), return_condition, Number(warranty_days),
91
+ makeCommitmentHash(pFields), makeDescriptionHash({ title, description, specs: specsJson }),
92
+ makePriceHash(priceNum, now), now,
93
+ commissionRateNum, product_type, imagesJsonForInsert
94
+ ]);
85
95
  // M7.2.6:免质押上架 — 不再扣 stake;首单成交时 settleOrder 自动从订单 escrow 锁定
86
96
  // M7.2-6: 上架时同步入 aliases(卖家已勾选的 candidates)
87
97
  if (Array.isArray(aliases) && aliases.length > 0) {
@@ -99,8 +109,7 @@ export function registerProductsCreateRoutes(app, deps) {
99
109
  if (type === 'title_substring' && !String(title).includes(value))
100
110
  continue;
101
111
  try {
102
- db.prepare(`INSERT INTO product_aliases (id, product_id, alias_type, alias_value, min_match_chars) VALUES (?,?,?,?,6)`)
103
- .run(generateId('pal'), id, type, value);
112
+ await dbRun(`INSERT INTO product_aliases (id, product_id, alias_type, alias_value, min_match_chars) VALUES (?,?,?,?,6)`, [generateId('pal'), id, type, value]);
104
113
  n++;
105
114
  }
106
115
  catch { }
@@ -110,48 +119,54 @@ export function registerProductsCreateRoutes(app, deps) {
110
119
  let linkConflict = null;
111
120
  if (source_url) {
112
121
  // 另一家卖家已认领此链接(verified=1)
113
- const otherClaim = db.prepare(`
122
+ const otherClaim = await dbOne(`
114
123
  SELECT pel.product_id FROM product_external_links pel
115
124
  JOIN products p ON pel.product_id = p.id
116
125
  WHERE pel.url = ? AND pel.verified = 1 AND p.seller_id != ?
117
- `).get(source_url, user.id);
126
+ `, [source_url, user.id]);
118
127
  if (otherClaim) {
119
- // 插入为未验证状态
120
- db.prepare(`INSERT OR IGNORE INTO product_external_links
121
- (id, product_id, url, source, verified, verify_note, platform, external_id, external_title)
122
- VALUES (?, ?, ?, 'import', 0, '链接冲突:等待众包验证确认归属', ?, ?, ?)`).run(generateId('lnk'), id, source_url, sourceMeta?.platform ?? null, sourceMeta?.external_id ?? null, externalTitleVal);
123
128
  // 创建认领验证任务(扣锁定费)
124
129
  const VERIFIERS_NEEDED = 1;
125
130
  const REWARD_EACH = 0.1;
126
131
  const feeLocked = VERIFIERS_NEEDED * REWARD_EACH;
127
- const walletNow = db.prepare('SELECT balance FROM wallets WHERE user_id = ?').get(user.id);
128
132
  const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
129
133
  const code = Array.from({ length: 8 }, () => chars[Math.floor(Math.random() * chars.length)]).join('');
134
+ const linkId2 = generateId('lnk');
130
135
  const taskId = generateId('vtk');
131
136
  const expiresAt = new Date(Date.now() + 72 * 3600_000).toISOString();
132
137
  const baseMsg = `此商品来源链接已被其他商家认领。请将验证码 [${code}] 放入该平台商品标题或描述,等待人工审核确认后归属自动转移。`;
133
- if (walletNow.balance >= feeLocked) {
134
- try {
138
+ // 冲突解析原子段:INSERT 冲突链接 + 商品转 warehouse + (余额够则)守恒扣验证费 + INSERT 验证任务。
139
+ // 扣费用 WHERE balance >= fee 守恒;不够则不建任务(feeOk=false → 手动验证文案),链接/仓库仍落。
140
+ let feeOk = false;
141
+ try {
142
+ feeOk = db.transaction(() => {
143
+ db.prepare(`INSERT OR IGNORE INTO product_external_links
144
+ (id, product_id, url, source, verified, verify_note, platform, external_id, external_title)
145
+ VALUES (?, ?, ?, 'import', 0, '链接冲突:等待众包验证确认归属', ?, ?, ?)`)
146
+ .run(linkId2, id, source_url, sourceMeta?.platform ?? null, sourceMeta?.external_id ?? null, externalTitleVal);
147
+ db.prepare(`UPDATE products SET status='warehouse', updated_at=datetime('now') WHERE id=?`).run(id);
148
+ const debit = db.prepare(`UPDATE wallets SET balance = balance - ? WHERE user_id = ? AND balance >= ?`).run(feeLocked, user.id, feeLocked);
149
+ if (debit.changes === 0)
150
+ return false;
135
151
  db.prepare(`INSERT INTO verify_tasks (id, type, product_id, url, code, verifiers_needed, reward_per_verifier, fee_locked, status, expires_at)
136
152
  VALUES (?,?,?,?,?,?,?,?,'code_issued',?)`).run(taskId, 'code_check', id, source_url, code, VERIFIERS_NEEDED, REWARD_EACH, feeLocked, expiresAt);
137
- db.prepare(`UPDATE wallets SET balance = balance - ? WHERE user_id = ?`).run(feeLocked, user.id);
138
- linkConflict = { task_id: taskId, code: `[${code}]`, expires_at: expiresAt, message: baseMsg };
139
- }
140
- catch {
141
- linkConflict = { message: `${baseMsg}(余额不足以锁定验证费 ${feeLocked} WAZ,请前往商品外部链接手动发起验证)` };
142
- }
153
+ return true;
154
+ })();
143
155
  }
144
- else {
145
- linkConflict = { message: `${baseMsg}(当前余额不足以锁定验证费 ${feeLocked} WAZ,请充值后前往商品编辑页手动发起验证)` };
156
+ catch (e) {
157
+ console.error('[products-create conflict tx]', e.message);
158
+ return void res.status(500).json({ error: '创建失败,请重试' });
146
159
  }
147
- // 有冲突:商品进入仓库,等待验证结果后再上架
148
- db.prepare(`UPDATE products SET status='warehouse', updated_at=datetime('now') WHERE id=?`).run(id);
160
+ linkConflict = feeOk
161
+ ? { task_id: taskId, code: `[${code}]`, expires_at: expiresAt, message: baseMsg }
162
+ : { message: `${baseMsg}(当前余额不足以锁定验证费 ${feeLocked} WAZ,请充值后前往商品编辑页手动发起验证)` };
149
163
  }
150
164
  else {
151
165
  // 无冲突 — 直接 verified=1
152
- db.prepare(`INSERT OR IGNORE INTO product_external_links
166
+ await dbRun(`INSERT OR IGNORE INTO product_external_links
153
167
  (id, product_id, url, source, verified, verified_at, platform, external_id, external_title)
154
- VALUES (?, ?, ?, 'import', 1, datetime('now'), ?, ?, ?)`).run(generateId('lnk'), id, source_url, sourceMeta?.platform ?? null, sourceMeta?.external_id ?? null, externalTitleVal);
168
+ VALUES (?, ?, ?, 'import', 1, datetime('now'), ?, ?, ?)`, [generateId('lnk'), id, source_url,
169
+ sourceMeta?.platform ?? null, sourceMeta?.external_id ?? null, externalTitleVal]);
155
170
  }
156
171
  }
157
172
  // 额外链接:同步冲突检查(最多 5 个)
@@ -161,25 +176,25 @@ export function registerProductsCreateRoutes(app, deps) {
161
176
  for (const extraUrl of additionalLinks.slice(0, 5)) {
162
177
  if (typeof extraUrl !== 'string' || !extraUrl.startsWith('http'))
163
178
  continue;
164
- const alreadyLinked = db.prepare('SELECT id FROM product_external_links WHERE product_id = ? AND url = ?').get(id, extraUrl);
179
+ const alreadyLinked = await dbOne('SELECT id FROM product_external_links WHERE product_id = ? AND url = ?', [id, extraUrl]);
165
180
  if (alreadyLinked)
166
181
  continue;
167
182
  // 同卖家已在其他商品关联过此链接
168
- const selfConflict = db.prepare(`
183
+ const selfConflict = await dbOne(`
169
184
  SELECT p.title FROM product_external_links pel
170
185
  JOIN products p ON pel.product_id = p.id
171
186
  WHERE pel.url = ? AND p.seller_id = ? AND p.id != ?
172
- `).get(extraUrl, user.id, id);
187
+ `, [extraUrl, user.id, id]);
173
188
  if (selfConflict) {
174
189
  blockedLinks.push({ url: extraUrl, message: `您已在商品「${selfConflict.title}」中关联了此链接` });
175
190
  continue;
176
191
  }
177
192
  // 他人已认领(verified=1)
178
- const otherConflict = db.prepare(`
193
+ const otherConflict = await dbOne(`
179
194
  SELECT p.title FROM product_external_links pel
180
195
  JOIN products p ON pel.product_id = p.id
181
196
  WHERE pel.url = ? AND pel.verified = 1 AND p.seller_id != ?
182
- `).get(extraUrl, user.id);
197
+ `, [extraUrl, user.id]);
183
198
  if (otherConflict) {
184
199
  blockedLinks.push({ url: extraUrl, message: `此链接已被其他商家认领,上架后可在商品编辑页发起验证任务` });
185
200
  continue;
@@ -187,9 +202,9 @@ export function registerProductsCreateRoutes(app, deps) {
187
202
  // 无冲突 — 直接 verified=1
188
203
  try {
189
204
  const extraMeta = parsePlatformUrl(extraUrl);
190
- db.prepare(`INSERT OR IGNORE INTO product_external_links
205
+ await dbRun(`INSERT OR IGNORE INTO product_external_links
191
206
  (id, product_id, url, source, verified, verified_at, platform, external_id)
192
- VALUES (?, ?, ?, 'import_extra', 1, datetime('now'), ?, ?)`).run(generateId('lnk'), id, extraUrl, extraMeta?.platform ?? null, extraMeta?.external_id ?? null);
207
+ VALUES (?, ?, ?, 'import_extra', 1, datetime('now'), ?, ?)`, [generateId('lnk'), id, extraUrl, extraMeta?.platform ?? null, extraMeta?.external_id ?? null]);
193
208
  }
194
209
  catch { }
195
210
  }
@@ -1,31 +1,32 @@
1
+ import { dbOne, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
1
2
  export function registerProductsCrudRoutes(app, deps) {
2
3
  const { db, auth, errorRes, formatProductForAgent, retireAnchorsByTarget } = deps;
3
4
  // 单品详情(agent verify price 时使用)
4
5
  // 卖家可查看自己的非上架商品(编辑页用),其他人只能看 active
5
- app.get('/api/products/:id', (req, res) => {
6
+ app.get('/api/products/:id', async (req, res) => {
6
7
  const token = (req.headers.authorization || '').replace('Bearer ', '');
7
- const selfUser = token ? db.prepare('SELECT id FROM users WHERE api_key = ?').get(token) : undefined;
8
- const row = db.prepare(`
8
+ const selfUser = token ? await dbOne('SELECT id FROM users WHERE api_key = ?', [token]) : undefined;
9
+ const row = await dbOne(`
9
10
  SELECT p.*, u.name as seller_name,
10
11
  COALESCE(rs.total_points, 0) as rep_points, COALESCE(rs.level, 'new') as rep_level
11
12
  FROM products p
12
13
  JOIN users u ON p.seller_id = u.id
13
14
  LEFT JOIN reputation_scores rs ON rs.user_id = p.seller_id
14
15
  WHERE p.id = ? AND (p.status = 'active' OR p.seller_id = ?)
15
- `).get(req.params.id, selfUser?.id ?? '');
16
+ `, [req.params.id, selfUser?.id ?? '']);
16
17
  if (!row)
17
18
  return void res.status(404).json({ error: 'not_found' });
18
19
  res.json(formatProductForAgent(row, req));
19
20
  });
20
21
  // 状态切换(active / warehouse / deleted)
21
- app.patch('/api/products/:id/status', (req, res) => {
22
+ app.patch('/api/products/:id/status', async (req, res) => {
22
23
  const user = auth(req, res);
23
24
  if (!user)
24
25
  return;
25
26
  const { status } = req.body;
26
27
  if (!['active', 'warehouse', 'deleted'].includes(status))
27
28
  return void res.json({ error: '无效状态值' });
28
- const product = db.prepare('SELECT id, claim_loss_count FROM products WHERE id = ? AND seller_id = ?').get(req.params.id, user.id);
29
+ const product = await dbOne('SELECT id, claim_loss_count FROM products WHERE id = ? AND seller_id = ?', [req.params.id, user.id]);
29
30
  if (!product)
30
31
  return void res.status(404).json({ error: '商品不存在或无权限' });
31
32
  if (status === 'active') {
@@ -33,33 +34,33 @@ export function registerProductsCrudRoutes(app, deps) {
33
34
  if ((product.claim_loss_count || 0) >= 3) {
34
35
  return void errorRes(res, 403, 'CLAIM_THRESHOLD_REACHED', `该商品累计 ${product.claim_loss_count} 次声明被验证不实,已达自动下架阈值。需 admin 干预方可重新上架,请联系管理员。`);
35
36
  }
36
- const pendingTask = db.prepare(`SELECT id FROM verify_tasks WHERE product_id=? AND status IN ('code_issued','open')`).get(req.params.id);
37
+ const pendingTask = await dbOne(`SELECT id FROM verify_tasks WHERE product_id=? AND status IN ('code_issued','open')`, [req.params.id]);
37
38
  if (pendingTask)
38
39
  return void res.json({ error: '链接核验进行中,请等待验证结果后再上架' });
39
- const hasRevoked = db.prepare(`SELECT id FROM product_external_links WHERE product_id=? AND revoked=1`).get(req.params.id);
40
- const hasValid = db.prepare(`SELECT id FROM product_external_links WHERE product_id=? AND verified=1 AND (revoked IS NULL OR revoked=0)`).get(req.params.id);
40
+ const hasRevoked = await dbOne(`SELECT id FROM product_external_links WHERE product_id=? AND revoked=1`, [req.params.id]);
41
+ const hasValid = await dbOne(`SELECT id FROM product_external_links WHERE product_id=? AND verified=1 AND (revoked IS NULL OR revoked=0)`, [req.params.id]);
41
42
  if (hasRevoked && !hasValid)
42
43
  return void res.json({ error: '所有外部链接已失效(主权失效),请先添加新链接后再上架' });
43
44
  }
44
- db.prepare(`UPDATE products SET status = ?, updated_at = datetime('now') WHERE id = ?`).run(status, req.params.id);
45
+ await dbRun(`UPDATE products SET status = ?, updated_at = datetime('now') WHERE id = ?`, [status, req.params.id]);
45
46
  res.json({ success: true });
46
47
  });
47
48
  // 硬删(仅 deleted 状态 + 无进行中订单)
48
- app.delete('/api/products/:id', (req, res) => {
49
+ app.delete('/api/products/:id', async (req, res) => {
49
50
  const user = auth(req, res);
50
51
  if (!user)
51
52
  return;
52
- const product = db.prepare('SELECT * FROM products WHERE id = ? AND seller_id = ?').get(req.params.id, user.id);
53
+ const product = await dbOne('SELECT * FROM products WHERE id = ? AND seller_id = ?', [req.params.id, user.id]);
53
54
  if (!product)
54
55
  return void res.status(404).json({ error: '商品不存在或无权限' });
55
56
  if (product.status !== 'deleted')
56
57
  return void res.json({ error: '请先将商品移入回收箱' });
57
- const activeOrders = db.prepare(`
58
+ const activeOrders = (await dbOne(`
58
59
  SELECT COUNT(*) as n FROM orders WHERE product_id = ? AND status NOT IN ('completed','cancelled','refunded','expired')
59
- `).get(req.params.id);
60
+ `, [req.params.id]));
60
61
  if (activeOrders.n > 0)
61
62
  return void res.json({ error: '该商品有进行中的订单,暂无法删除' });
62
- db.prepare('DELETE FROM product_external_links WHERE product_id = ?').run(req.params.id);
63
+ await dbRun('DELETE FROM product_external_links WHERE product_id = ?', [req.params.id]);
63
64
  // E1 anchor GC: 指向该 product 的 active anchor → retired
64
65
  try {
65
66
  retireAnchorsByTarget(db, 'product', String(req.params.id));
@@ -67,7 +68,7 @@ export function registerProductsCrudRoutes(app, deps) {
67
68
  catch (e) {
68
69
  console.warn('[anchor-gc product]', e.message);
69
70
  }
70
- db.prepare('DELETE FROM products WHERE id = ?').run(req.params.id);
71
+ await dbRun('DELETE FROM products WHERE id = ?', [req.params.id]);
71
72
  res.json({ success: true });
72
73
  });
73
74
  }
@@ -1,23 +1,26 @@
1
+ import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
1
2
  export function registerProductsLinksRoutes(app, deps) {
3
+ // 只读/单写站点走 RFC-016 异步 seam;db 保留:认领冲突分支是 fee-lock 资金路径,
4
+ // INSERT 链接 + INSERT 验证任务 + 钱包扣费必须原子(db.transaction + 守恒/dup guard),Phase 3 迁 pg 行锁。
2
5
  const { db, auth, generateId, extractUrlFromText, extractTitleFromText, parsePlatformUrl } = deps;
3
- app.get('/api/products/:id/links', (req, res) => {
6
+ app.get('/api/products/:id/links', async (req, res) => {
4
7
  const user = auth(req, res);
5
8
  if (!user)
6
9
  return;
7
- const product = db.prepare('SELECT seller_id FROM products WHERE id = ?').get(req.params.id);
10
+ const product = await dbOne('SELECT seller_id FROM products WHERE id = ?', [req.params.id]);
8
11
  if (!product)
9
12
  return void res.status(404).json({ error: '商品不存在' });
10
13
  if (product.seller_id !== user.id)
11
14
  return void res.status(403).json({ error: '无权限' });
12
- const links = db.prepare(`SELECT id, url, source, verified, revoked, verify_note, added_at, platform, external_id, external_title FROM product_external_links WHERE product_id = ? ORDER BY added_at ASC`).all(req.params.id);
15
+ const links = await dbAll(`SELECT id, url, source, verified, revoked, verify_note, added_at, platform, external_id, external_title FROM product_external_links WHERE product_id = ? ORDER BY added_at ASC`, [req.params.id]);
13
16
  res.json(links);
14
17
  });
15
18
  // 新链接(无人认领)直接 verified=1;已被他人认领则发起众包验证任务
16
- app.post('/api/products/:id/links', (req, res) => {
19
+ app.post('/api/products/:id/links', async (req, res) => {
17
20
  const user = auth(req, res);
18
21
  if (!user)
19
22
  return;
20
- const product = db.prepare('SELECT * FROM products WHERE id = ? AND seller_id = ?').get(req.params.id, user.id);
23
+ const product = await dbOne('SELECT * FROM products WHERE id = ? AND seller_id = ?', [req.params.id, user.id]);
21
24
  if (!product)
22
25
  return void res.status(404).json({ error: '商品不存在或无权限' });
23
26
  // 支持两种 body:(a) {url, external_title?} (b) {text}
@@ -34,44 +37,42 @@ export function registerProductsLinksRoutes(app, deps) {
34
37
  // 精准匹配原则:external_title 必须显式输入,不 fallback 到 product.title
35
38
  const linkExternalTitle = bodyExternalTitle && bodyExternalTitle.trim() ? bodyExternalTitle.trim() : null;
36
39
  // 已关联此商品
37
- const existing = db.prepare('SELECT id, verified, revoked FROM product_external_links WHERE product_id = ? AND url = ?')
38
- .get(req.params.id, url);
40
+ const existing = await dbOne('SELECT id, verified, revoked FROM product_external_links WHERE product_id = ? AND url = ?', [req.params.id, url]);
39
41
  if (existing) {
40
42
  // 主权失效的旧记录:删除后允许重新发起认领
41
43
  if (existing.revoked) {
42
- db.prepare('DELETE FROM product_external_links WHERE id = ?').run(existing.id);
44
+ await dbRun('DELETE FROM product_external_links WHERE id = ?', [existing.id]);
43
45
  }
44
46
  else {
45
47
  return void res.json({ error: '该链接已关联到此商品' });
46
48
  }
47
49
  }
48
50
  // 同卖家的其他商品已关联此链接
49
- const sameSellerOther = db.prepare(`
51
+ const sameSellerOther = await dbOne(`
50
52
  SELECT p.title FROM product_external_links pel
51
53
  JOIN products p ON pel.product_id = p.id
52
54
  WHERE pel.url = ? AND p.seller_id = ? AND pel.product_id != ?
53
- `).get(url, user.id, req.params.id);
55
+ `, [url, user.id, req.params.id]);
54
56
  if (sameSellerOther) {
55
57
  return void res.json({ error: `此链接已在您的商品「${sameSellerOther.title}」中关联,一个链接不能关联多个商品` });
56
58
  }
57
59
  // 是否已被其他卖家 verified 认领
58
- const otherClaim = db.prepare(`
60
+ const otherClaim = await dbOne(`
59
61
  SELECT p.title as product_title FROM product_external_links pel
60
62
  JOIN products p ON pel.product_id = p.id
61
63
  WHERE pel.url = ? AND pel.verified = 1 AND p.seller_id != ?
62
- `).get(url, user.id);
64
+ `, [url, user.id]);
63
65
  if (!otherClaim) {
64
66
  // 新链接,无冲突:直接 verified=1
65
67
  const linkId = generateId('lnk');
66
68
  const meta = parsePlatformUrl(url);
67
- db.prepare(`INSERT INTO product_external_links
69
+ await dbRun(`INSERT INTO product_external_links
68
70
  (id, product_id, url, source, verified, verified_at, platform, external_id, external_title)
69
- VALUES (?, ?, ?, 'manual', 1, datetime('now'), ?, ?, ?)`).run(linkId, req.params.id, url, meta?.platform ?? null, meta?.external_id ?? null, linkExternalTitle);
71
+ VALUES (?, ?, ?, 'manual', 1, datetime('now'), ?, ?, ?)`, [linkId, req.params.id, url, meta?.platform ?? null, meta?.external_id ?? null, linkExternalTitle]);
70
72
  return void res.json({ link_id: linkId, verified: 1, external_title: linkExternalTitle, message: '链接已关联' });
71
73
  }
72
74
  // 已被他人认领:发起众包验证任务
73
- const existingTask = db.prepare(`SELECT id, code, status, expires_at FROM verify_tasks WHERE product_id = ? AND url = ? AND status IN ('code_issued','open')`)
74
- .get(req.params.id, url);
75
+ const existingTask = await dbOne(`SELECT id, code, status, expires_at FROM verify_tasks WHERE product_id = ? AND url = ? AND status IN ('code_issued','open')`, [req.params.id, url]);
75
76
  if (existingTask) {
76
77
  const isPending = existingTask.status === 'code_issued';
77
78
  return void res.json({
@@ -89,7 +90,8 @@ export function registerProductsLinksRoutes(app, deps) {
89
90
  const VERIFIERS_NEEDED = 1;
90
91
  const REWARD_EACH = 0.1;
91
92
  const feeLocked = VERIFIERS_NEEDED * REWARD_EACH;
92
- const wallet = db.prepare('SELECT balance FROM wallets WHERE user_id = ?').get(user.id);
93
+ // 友好预检查(读):真正的守恒门在事务内(WHERE balance >= feeLocked)
94
+ const wallet = (await dbOne('SELECT balance FROM wallets WHERE user_id = ?', [user.id]));
93
95
  if (wallet.balance < feeLocked) {
94
96
  return void res.json({ error: `余额不足:认领验证需锁定 ${feeLocked} WAZ,当前余额 ${wallet.balance} WAZ` });
95
97
  }
@@ -100,12 +102,33 @@ export function registerProductsLinksRoutes(app, deps) {
100
102
  const taskId = generateId('vtk');
101
103
  const expiresAt = new Date(Date.now() + 72 * 3600_000).toISOString();
102
104
  const claimMeta = parsePlatformUrl(url);
103
- db.prepare(`INSERT INTO product_external_links
104
- (id, product_id, url, source, verified, verify_note, platform, external_id, external_title)
105
- VALUES (?, ?, ?, 'manual', 0, '认领验证进行中', ?, ?, ?)`).run(linkId, req.params.id, url, claimMeta?.platform ?? null, claimMeta?.external_id ?? null, linkExternalTitle);
106
- db.prepare(`INSERT INTO verify_tasks (id, type, product_id, url, code, verifiers_needed, reward_per_verifier, fee_locked, status, expires_at)
107
- VALUES (?,?,?,?,?,?,?,?,'code_issued',?)`).run(taskId, 'claim', req.params.id, url, code, VERIFIERS_NEEDED, REWARD_EACH, feeLocked, expiresAt);
108
- db.prepare(`UPDATE wallets SET balance = balance - ? WHERE user_id = ?`).run(feeLocked, user.id);
105
+ // fee-lock 原子段:重检无进行中任务(防双任务双锁费)+ 钱包扣费(守恒 guard)+ INSERT 链接 + INSERT 验证任务。
106
+ try {
107
+ db.transaction(() => {
108
+ const dupTask = db.prepare(`SELECT id FROM verify_tasks WHERE product_id = ? AND url = ? AND status IN ('code_issued','open')`).get(req.params.id, url);
109
+ if (dupTask)
110
+ throw new Error('LINK_TASK_EXISTS');
111
+ const debit = db.prepare(`UPDATE wallets SET balance = balance - ? WHERE user_id = ? AND balance >= ?`).run(feeLocked, user.id, feeLocked);
112
+ if (debit.changes === 0)
113
+ throw new Error('LINK_INSUFFICIENT');
114
+ db.prepare(`INSERT INTO product_external_links
115
+ (id, product_id, url, source, verified, verify_note, platform, external_id, external_title)
116
+ VALUES (?, ?, ?, 'manual', 0, '认领验证进行中', ?, ?, ?)`)
117
+ .run(linkId, req.params.id, url, claimMeta?.platform ?? null, claimMeta?.external_id ?? null, linkExternalTitle);
118
+ db.prepare(`INSERT INTO verify_tasks (id, type, product_id, url, code, verifiers_needed, reward_per_verifier, fee_locked, status, expires_at)
119
+ VALUES (?,?,?,?,?,?,?,?,'code_issued',?)`)
120
+ .run(taskId, 'claim', req.params.id, url, code, VERIFIERS_NEEDED, REWARD_EACH, feeLocked, expiresAt);
121
+ })();
122
+ }
123
+ catch (e) {
124
+ const msg = e.message;
125
+ if (msg === 'LINK_TASK_EXISTS')
126
+ return void res.json({ error: '此链接已有进行中的认领任务,请刷新页面查看' });
127
+ if (msg === 'LINK_INSUFFICIENT')
128
+ return void res.json({ error: `余额不足:认领验证需锁定 ${feeLocked} WAZ` });
129
+ console.error('[products-links claim tx]', msg);
130
+ return void res.status(500).json({ error: '发起认领失败,请重试' });
131
+ }
109
132
  res.json({
110
133
  link_id: linkId,
111
134
  task_id: taskId,
@@ -116,14 +139,14 @@ export function registerProductsLinksRoutes(app, deps) {
116
139
  expires_at: expiresAt,
117
140
  });
118
141
  });
119
- app.delete('/api/products/:id/links/:linkId', (req, res) => {
142
+ app.delete('/api/products/:id/links/:linkId', async (req, res) => {
120
143
  const user = auth(req, res);
121
144
  if (!user)
122
145
  return;
123
- const product = db.prepare('SELECT seller_id FROM products WHERE id = ?').get(req.params.id);
146
+ const product = await dbOne('SELECT seller_id FROM products WHERE id = ?', [req.params.id]);
124
147
  if (!product || product.seller_id !== user.id)
125
148
  return void res.status(403).json({ error: '无权限' });
126
- db.prepare('DELETE FROM product_external_links WHERE id = ? AND product_id = ?').run(req.params.linkId, req.params.id);
149
+ await dbRun('DELETE FROM product_external_links WHERE id = ? AND product_id = ?', [req.params.linkId, req.params.id]);
127
150
  res.json({ success: true });
128
151
  });
129
152
  }
@@ -1,7 +1,9 @@
1
1
  import { createHmac } from 'crypto';
2
+ import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
2
3
  export function registerProductsListRoutes(app, deps) {
3
- const { db, getUser, VALID_PRODUCT_TYPES, RAW_MODE_MIN_TRUST, getAgentTrustCached, VALID_SORTS, PRODUCT_LIMITS, TRENDING_SCORE_EXPR, findProductsByAlias, decodeProductCursor, encodeProductCursor, MASTER_SEED, formatProductForAgent } = deps;
4
- app.get('/api/products', (req, res) => {
4
+ // db 已走 RFC-016 异步 seam(dbOne/dbAll),不再直接用 deps.db
5
+ const { getUser, VALID_PRODUCT_TYPES, RAW_MODE_MIN_TRUST, getAgentTrustCached, VALID_SORTS, PRODUCT_LIMITS, TRENDING_SCORE_EXPR, findProductsByAlias, decodeProductCursor, encodeProductCursor, MASTER_SEED, formatProductForAgent } = deps;
6
+ app.get('/api/products', async (req, res) => {
5
7
  const { q = '', category, max_price, min_return_days, max_handling_hours, has_sales, ship_to, mode: modeRaw = 'pwa', sort: sortRaw, cursor, limit: limitRaw, seller_id, product_type: productTypeRaw, fuzzy: fuzzyRaw, since_days: sinceDaysRaw } = req.query;
6
8
  let mode = modeRaw === 'agent' ? 'agent' : (modeRaw === 'raw' ? 'raw' : 'pwa');
7
9
  // fuzzy=true → 发现页用,模糊 LIKE;否则用协议级 alias 精确匹配(智能下单页)
@@ -213,7 +215,7 @@ export function registerProductsListRoutes(app, deps) {
213
215
  const finalParams = [...params, ...cursorParams, buffer];
214
216
  let candidates;
215
217
  try {
216
- candidates = db.prepare(sql).all(...finalParams);
218
+ candidates = await dbAll(sql, finalParams);
217
219
  }
218
220
  catch (e) {
219
221
  console.error('[/api/products] sql error:', e, '\nSQL:', sql);
@@ -296,7 +298,7 @@ export function registerProductsListRoutes(app, deps) {
296
298
  nextCursor = encodeProductCursor(Number(anchor.trending_score) || 0, String(anchor.id));
297
299
  }
298
300
  else {
299
- const jd = db.prepare(`SELECT julianday(?) as j`).get(anchor.created_at).j;
301
+ const jd = (await dbOne(`SELECT julianday(?) as j`, [anchor.created_at])).j;
300
302
  nextCursor = encodeProductCursor(jd, String(anchor.id));
301
303
  }
302
304
  }