@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.
- package/README.md +5 -1
- package/dist/layer0-foundation/L0-1-database/db-backends/pg-backend.js +51 -0
- package/dist/layer0-foundation/L0-1-database/db-backends/sql-dialect-datetime.js +437 -0
- package/dist/layer0-foundation/L0-1-database/db-backends/sql-placeholders.js +98 -0
- package/dist/layer0-foundation/L0-1-database/db.js +65 -0
- package/dist/layer0-foundation/L0-2-state-machine/order-chain.js +13 -11
- package/dist/layer0-foundation/L0-2-state-machine/transitions.js +1 -1
- package/dist/layer0-foundation/L0-5-manifest/manifest.js +13 -11
- package/dist/layer1-agent/L1-1-mcp-server/server.js +288 -208
- package/dist/layer1-agent/L1-2-external-anchor/anchor-engine.js +14 -12
- package/dist/layer2-business/L2-6-notifications/notification-engine.js +8 -5
- package/dist/layer2-business/L2-7-snf/snf-engine.js +16 -14
- package/dist/layer2-business/L2-8-feedback/build-feedback-engine.js +18 -10
- package/dist/layer2-business/L2-9-contribution/build-reputation-engine.js +37 -23
- package/dist/layer2-business/L2-9-contribution/build-task-agent-metadata-store.js +182 -0
- package/dist/layer2-business/L2-9-contribution/build-task-participation.js +47 -0
- package/dist/layer2-business/L2-9-contribution/build-task-read.js +222 -0
- package/dist/layer2-business/L2-9-contribution/build-tasks-engine.js +11 -3
- package/dist/layer2-business/L2-9-contribution/canonical-contribution-target.js +16 -0
- package/dist/layer2-business/L2-9-contribution/contribution-display-envelope.js +40 -0
- package/dist/layer2-business/L2-9-contribution/contribution-score-contract.js +36 -0
- package/dist/layer2-business/L2-9-contribution/contribution-score-evidence.js +61 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/canonical.js +60 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/github-credential.schema.js +140 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/github-fetch-adapter.js +437 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/self-consistency.js +38 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/verifier.js +231 -0
- package/dist/layer2-business/L2-9-contribution/github-credential-ingestion-engine.js +145 -0
- package/dist/layer2-business/L2-9-contribution/github-credential-store.js +115 -0
- package/dist/layer2-business/L2-9-contribution/identity-binding-engine.js +134 -0
- package/dist/layer2-business/L2-9-contribution/identity-binding-store.js +101 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-challenge-engine.js +126 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-challenge-store.js +30 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-discovery.js +55 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-engine.js +109 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-fact-precondition.js +22 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-proof-verifier.js +97 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-read.js +59 -0
- package/dist/layer2-business/L2-9-contribution/task-proposal-ai-store.js +99 -0
- package/dist/layer2-business/L2-9-contribution/task-proposal-draft.js +191 -0
- package/dist/layer2-business/L2-9-contribution/task-proposal-store.js +129 -0
- package/dist/layer2-business/L2-notes/note-photo-storage.js +4 -2
- package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +17 -15
- package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +11 -8
- package/dist/layer4-economics/L4-3-reputation/reputation-engine.js +9 -8
- package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +11 -8
- package/dist/layer4-economics/L4-4-skill-market/skill-listing-engine.js +22 -16
- package/dist/pwa/acp-feed.js +13 -1
- package/dist/pwa/admin-bearer-auth.js +21 -0
- package/dist/pwa/contract-fingerprint.js +2 -0
- package/dist/pwa/email-delivery.js +127 -0
- package/dist/pwa/endpoint-actions.js +5 -1
- package/dist/pwa/goal-index.js +8 -8
- package/dist/pwa/human-presence.js +62 -0
- package/dist/pwa/public/app.js +1485 -283
- package/dist/pwa/public/i18n.js +297 -59
- package/dist/pwa/public/index.html +1 -0
- package/dist/pwa/public/openapi.json +5 -5
- package/dist/pwa/public/whitepaper/en/index.html +153 -0
- package/dist/pwa/public/whitepaper/zh-CN/index.html +153 -0
- package/dist/pwa/rate-limit.js +22 -0
- package/dist/pwa/routes/account-deletion.js +15 -13
- package/dist/pwa/routes/addresses.js +10 -9
- package/dist/pwa/routes/admin-admins.js +13 -14
- package/dist/pwa/routes/admin-analytics.js +109 -69
- package/dist/pwa/routes/admin-atomic.js +10 -4
- package/dist/pwa/routes/admin-catalog.js +13 -11
- package/dist/pwa/routes/admin-editor-picks.js +15 -10
- package/dist/pwa/routes/admin-events.js +5 -3
- package/dist/pwa/routes/admin-health.js +2 -1
- package/dist/pwa/routes/admin-moderation.js +50 -29
- package/dist/pwa/routes/admin-ops.js +35 -23
- package/dist/pwa/routes/admin-protocol-params.js +16 -19
- package/dist/pwa/routes/admin-reports.js +23 -21
- package/dist/pwa/routes/admin-tokenomics.js +26 -25
- package/dist/pwa/routes/admin-users-lifecycle.js +37 -40
- package/dist/pwa/routes/admin-users-query.js +65 -53
- package/dist/pwa/routes/admin-verifier-flow.js +82 -41
- package/dist/pwa/routes/admin-verifier-whitelist.js +55 -27
- package/dist/pwa/routes/admin-wallet-ops.js +32 -7
- package/dist/pwa/routes/agent-buy.js +46 -22
- package/dist/pwa/routes/agent-governance.js +52 -56
- package/dist/pwa/routes/ai.js +7 -5
- package/dist/pwa/routes/analytics.js +43 -41
- package/dist/pwa/routes/anchors.js +19 -20
- package/dist/pwa/routes/announcements.js +13 -13
- package/dist/pwa/routes/arbitrator.js +97 -31
- package/dist/pwa/routes/auction.js +157 -116
- package/dist/pwa/routes/auth-login.js +6 -4
- package/dist/pwa/routes/auth-read.js +21 -10
- package/dist/pwa/routes/auth-register.js +111 -26
- package/dist/pwa/routes/auth-sessions.js +12 -11
- package/dist/pwa/routes/blocklist.js +16 -15
- package/dist/pwa/routes/build-feedback.js +10 -9
- package/dist/pwa/routes/build-reputation.js +6 -2
- package/dist/pwa/routes/build-tasks.js +45 -13
- package/dist/pwa/routes/buyer-feeds.js +27 -25
- package/dist/pwa/routes/cart.js +16 -15
- package/dist/pwa/routes/charity.js +212 -150
- package/dist/pwa/routes/chat.js +42 -43
- package/dist/pwa/routes/checkin-tasks.js +10 -9
- package/dist/pwa/routes/checkout-helpers.js +12 -10
- package/dist/pwa/routes/claim-initiators.js +34 -14
- package/dist/pwa/routes/claim-verify.js +86 -53
- package/dist/pwa/routes/claim-voting.js +43 -18
- package/dist/pwa/routes/contribution-identity.js +164 -0
- package/dist/pwa/routes/contribution-score.js +19 -0
- package/dist/pwa/routes/coupons.js +19 -16
- package/dist/pwa/routes/dashboards.js +18 -16
- package/dist/pwa/routes/dispute-cases.js +25 -24
- package/dist/pwa/routes/disputes-read.js +45 -51
- package/dist/pwa/routes/disputes-write.js +124 -61
- package/dist/pwa/routes/evidence.js +9 -9
- package/dist/pwa/routes/external-anchors.js +13 -12
- package/dist/pwa/routes/feedback.js +29 -33
- package/dist/pwa/routes/flash-sales.js +18 -16
- package/dist/pwa/routes/follows.js +25 -24
- package/dist/pwa/routes/governance-auto-deactivate.js +21 -9
- package/dist/pwa/routes/governance-onboarding.js +70 -59
- package/dist/pwa/routes/group-buys.js +22 -22
- package/dist/pwa/routes/growth.js +34 -31
- package/dist/pwa/routes/import-product.js +12 -10
- package/dist/pwa/routes/kyc.js +9 -8
- package/dist/pwa/routes/leaderboard.js +20 -18
- package/dist/pwa/routes/listings.js +23 -22
- package/dist/pwa/routes/logistics.js +10 -8
- package/dist/pwa/routes/manifests.js +27 -27
- package/dist/pwa/routes/me-data.js +23 -21
- package/dist/pwa/routes/notifications.js +7 -6
- package/dist/pwa/routes/offers.js +30 -12
- package/dist/pwa/routes/orders-action.js +51 -29
- package/dist/pwa/routes/orders-create.js +75 -20
- package/dist/pwa/routes/orders-read.js +21 -20
- package/dist/pwa/routes/p2p-products.js +30 -18
- package/dist/pwa/routes/payments-governance.js +61 -56
- package/dist/pwa/routes/peers.js +9 -8
- package/dist/pwa/routes/pin-receipts.js +13 -13
- package/dist/pwa/routes/products-aliases.js +12 -10
- package/dist/pwa/routes/products-claims.js +36 -17
- package/dist/pwa/routes/products-create.js +53 -38
- package/dist/pwa/routes/products-crud.js +17 -16
- package/dist/pwa/routes/products-links.js +49 -26
- package/dist/pwa/routes/products-list.js +6 -4
- package/dist/pwa/routes/products-meta.js +40 -39
- package/dist/pwa/routes/products-update.js +19 -5
- package/dist/pwa/routes/profile-credentials.js +20 -19
- package/dist/pwa/routes/profile-identity.js +14 -13
- package/dist/pwa/routes/profile-location.js +7 -6
- package/dist/pwa/routes/profile-placement.js +20 -19
- package/dist/pwa/routes/profile-prefs.js +11 -11
- package/dist/pwa/routes/promoter.js +58 -66
- package/dist/pwa/routes/public-build-tasks.js +19 -0
- package/dist/pwa/routes/public-utils.js +108 -46
- package/dist/pwa/routes/push.js +16 -15
- package/dist/pwa/routes/ratings.js +92 -32
- package/dist/pwa/routes/recover-key.js +66 -26
- package/dist/pwa/routes/referral.js +37 -52
- package/dist/pwa/routes/reputation.js +3 -2
- package/dist/pwa/routes/returns.js +76 -73
- package/dist/pwa/routes/reviews.js +41 -18
- package/dist/pwa/routes/rewards-apply.js +16 -15
- package/dist/pwa/routes/rewards-auto-downgrade.js +9 -7
- package/dist/pwa/routes/rewards-escrow-expire.js +7 -5
- package/dist/pwa/routes/rfqs.js +163 -85
- package/dist/pwa/routes/search.js +16 -14
- package/dist/pwa/routes/secondhand.js +25 -22
- package/dist/pwa/routes/seller-quota.js +24 -26
- package/dist/pwa/routes/share-redirects.js +60 -55
- package/dist/pwa/routes/shareables-interactions.js +34 -35
- package/dist/pwa/routes/shareables.js +55 -51
- package/dist/pwa/routes/shop-referral.js +58 -0
- package/dist/pwa/routes/shops.js +25 -20
- package/dist/pwa/routes/signaling.js +10 -9
- package/dist/pwa/routes/skill-market.js +16 -16
- package/dist/pwa/routes/skills.js +15 -14
- package/dist/pwa/routes/snf.js +14 -13
- package/dist/pwa/routes/tags.js +10 -9
- package/dist/pwa/routes/task-proposals.js +121 -0
- package/dist/pwa/routes/trial.js +72 -52
- package/dist/pwa/routes/trusted-kpi.js +20 -18
- package/dist/pwa/routes/url-claim.js +67 -28
- package/dist/pwa/routes/users-public.js +62 -70
- package/dist/pwa/routes/variants.js +12 -13
- package/dist/pwa/routes/verifier-user.js +61 -21
- package/dist/pwa/routes/verify-tasks.js +49 -25
- package/dist/pwa/routes/waitlist.js +16 -15
- package/dist/pwa/routes/wallet-read.js +75 -37
- package/dist/pwa/routes/wallet-write.js +12 -9
- package/dist/pwa/routes/webauthn.js +25 -26
- package/dist/pwa/routes/webhooks.js +26 -26
- package/dist/pwa/routes/welcome.js +45 -50
- package/dist/pwa/routes/wishlist-qa.js +29 -32
- package/dist/pwa/server.js +304 -90
- package/dist/version.js +1 -1
- package/package.json +76 -3
|
@@ -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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
134
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
catch {
|
|
141
|
-
linkConflict = { message: `${baseMsg}(余额不足以锁定验证费 ${feeLocked} WAZ,请前往商品外部链接手动发起验证)` };
|
|
142
|
-
}
|
|
153
|
+
return true;
|
|
154
|
+
})();
|
|
143
155
|
}
|
|
144
|
-
|
|
145
|
-
|
|
156
|
+
catch (e) {
|
|
157
|
+
console.error('[products-create conflict tx]', e.message);
|
|
158
|
+
return void res.status(500).json({ error: '创建失败,请重试' });
|
|
146
159
|
}
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
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'), ?, ?, ?)
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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'), ?, ?)
|
|
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 ?
|
|
8
|
-
const row =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
40
|
-
const hasValid =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
60
|
+
`, [req.params.id]));
|
|
60
61
|
if (activeOrders.n > 0)
|
|
61
62
|
return void res.json({ error: '该商品有进行中的订单,暂无法删除' });
|
|
62
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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'), ?, ?, ?)
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
4
|
-
|
|
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 =
|
|
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 =
|
|
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
|
}
|