@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.
- package/README.md +2 -0
- 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 +198 -83
- 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 +173 -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 +10 -2
- 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-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-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/contract-fingerprint.js +2 -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 +575 -68
- package/dist/pwa/public/i18n.js +29 -20
- package/dist/pwa/public/index.html +1 -0
- package/dist/pwa/public/openapi.json +2 -2
- 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-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 +26 -29
- package/dist/pwa/routes/admin-ops.js +22 -21
- 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 +54 -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 +7 -5
- 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 +153 -114
- package/dist/pwa/routes/auth-login.js +6 -4
- package/dist/pwa/routes/auth-read.js +11 -9
- package/dist/pwa/routes/auth-register.js +35 -20
- 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 +147 -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 +33 -30
- 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 +33 -17
- 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 +14 -16
- 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 +19 -17
- package/dist/pwa/routes/profile-prefs.js +11 -11
- package/dist/pwa/routes/promoter.js +55 -49
- 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 +30 -30
- package/dist/pwa/routes/recover-key.js +13 -12
- package/dist/pwa/routes/referral.js +37 -32
- 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 +59 -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 +57 -0
- package/dist/pwa/routes/shops.js +20 -18
- 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 +45 -0
- package/dist/pwa/routes/trial.js +69 -51
- 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 -60
- 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 +74 -36
- 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 +237 -81
- package/dist/version.js +1 -1
- package/package.json +47 -2
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
const GROWTH_TASK_CATALOG = [
|
|
2
3
|
// 第 1 关:新手起步
|
|
3
4
|
{ id: 'first_purchase', chapter: 1,
|
|
@@ -59,8 +60,9 @@ const GROWTH_TASK_CATALOG = [
|
|
|
59
60
|
desc_zh: '正式跻身分享达人', desc_en: 'Officially a share pro',
|
|
60
61
|
evaluate: c => c.team_total >= 50 },
|
|
61
62
|
];
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
// RFC-016: db 参数保留(签名兼容),内部走异步 seam(同实例,setSeamDb)
|
|
64
|
+
async function buildGrowthTaskCtx(_db, userId) {
|
|
65
|
+
const u = await dbOne("SELECT bio, search_anchor, default_address_json, default_address_text FROM users WHERE id = ?", [userId]);
|
|
64
66
|
let line1 = null;
|
|
65
67
|
try {
|
|
66
68
|
const a = JSON.parse(u?.default_address_json || 'null');
|
|
@@ -69,21 +71,21 @@ function buildGrowthTaskCtx(db, userId) {
|
|
|
69
71
|
catch { }
|
|
70
72
|
if (!line1 && u?.default_address_text)
|
|
71
73
|
line1 = u.default_address_text; // legacy 兜底
|
|
72
|
-
const completed =
|
|
73
|
-
const l1 =
|
|
74
|
-
const teamTotal =
|
|
74
|
+
const completed = (await dbOne("SELECT COUNT(*) AS n FROM orders WHERE buyer_id = ? AND status = 'completed'", [userId])).n;
|
|
75
|
+
const l1 = (await dbOne("SELECT COUNT(*) AS n FROM users WHERE sponsor_id = ?", [userId])).n;
|
|
76
|
+
const teamTotal = (await dbOne(`
|
|
75
77
|
SELECT COUNT(*) AS n FROM users
|
|
76
78
|
WHERE sponsor_id = ?
|
|
77
79
|
OR sponsor_id IN (SELECT id FROM users WHERE sponsor_id = ?)
|
|
78
80
|
OR sponsor_id IN (SELECT id FROM users WHERE sponsor_id IN (SELECT id FROM users WHERE sponsor_id = ?))
|
|
79
|
-
|
|
80
|
-
const grand =
|
|
81
|
-
const sCount =
|
|
82
|
-
const mCount =
|
|
83
|
-
const pv =
|
|
81
|
+
`, [userId, userId, userId])).n;
|
|
82
|
+
const grand = (await dbOne("SELECT COALESCE(SUM(amount),0) AS s FROM commission_records WHERE beneficiary_id = ?", [userId])).s;
|
|
83
|
+
const sCount = (await dbOne("SELECT COUNT(*) AS n FROM shareables WHERE owner_id = ? AND status = 'active'", [userId])).n;
|
|
84
|
+
const mCount = (await dbOne("SELECT COUNT(*) AS n FROM manifest_registry WHERE owner_id = ? AND status = 'active'", [userId])).n;
|
|
85
|
+
const pv = await dbOne("SELECT total_left_pv, total_right_pv FROM users WHERE id = ?", [userId]);
|
|
84
86
|
const weakLeg = Math.min(Number(pv?.total_left_pv || 0), Number(pv?.total_right_pv || 0));
|
|
85
|
-
const comm30 =
|
|
86
|
-
const waz30 =
|
|
87
|
+
const comm30 = (await dbOne(`SELECT COALESCE(SUM(amount),0) AS s FROM commission_records WHERE beneficiary_id = ? AND created_at >= datetime('now','-30 days')`, [userId])).s;
|
|
88
|
+
const waz30 = (await dbOne(`SELECT COALESCE(SUM(waz_amount),0) AS s FROM binary_score_records WHERE user_id = ? AND settled_at >= datetime('now','-30 days')`, [userId])).s;
|
|
87
89
|
return {
|
|
88
90
|
userId,
|
|
89
91
|
bio: u?.bio || null,
|
|
@@ -99,9 +101,9 @@ function buildGrowthTaskCtx(db, userId) {
|
|
|
99
101
|
last_30_total: comm30 + waz30,
|
|
100
102
|
};
|
|
101
103
|
}
|
|
102
|
-
function evaluateGrowthTasks(
|
|
103
|
-
const ctx = buildGrowthTaskCtx(
|
|
104
|
-
const logs =
|
|
104
|
+
async function evaluateGrowthTasks(_db, userId, lang = 'zh') {
|
|
105
|
+
const ctx = await buildGrowthTaskCtx(_db, userId);
|
|
106
|
+
const logs = await dbAll("SELECT task_id, status, claimed_at, completed_at FROM growth_task_log WHERE user_id = ?", [userId]);
|
|
105
107
|
const logMap = new Map(logs.map(l => [l.task_id, l]));
|
|
106
108
|
const out = [];
|
|
107
109
|
for (const t of GROWTH_TASK_CATALOG) {
|
|
@@ -111,8 +113,8 @@ function evaluateGrowthTasks(db, userId, lang = 'zh') {
|
|
|
111
113
|
let completed_at = log?.completed_at || null;
|
|
112
114
|
if (done) {
|
|
113
115
|
if (!log || log.status !== 'completed') {
|
|
114
|
-
|
|
115
|
-
VALUES (?,?,?,?,datetime('now'))
|
|
116
|
+
await dbRun(`INSERT OR REPLACE INTO growth_task_log (user_id, task_id, status, claimed_at, completed_at)
|
|
117
|
+
VALUES (?,?,?,?,datetime('now'))`, [userId, t.id, 'completed', log?.claimed_at || null]);
|
|
116
118
|
completed_at = new Date().toISOString().slice(0, 19).replace('T', ' ');
|
|
117
119
|
}
|
|
118
120
|
status = 'completed';
|
|
@@ -147,53 +149,54 @@ function evaluateGrowthTasks(db, userId, lang = 'zh') {
|
|
|
147
149
|
return { tasks: out, summary };
|
|
148
150
|
}
|
|
149
151
|
export function registerGrowthRoutes(app, deps) {
|
|
152
|
+
// db 仍在 destructure 中(传给 evaluateGrowthTasks 的签名);本文件 handler 走 RFC-016 异步 seam
|
|
150
153
|
const { db, auth } = deps;
|
|
151
|
-
app.get('/api/growth/tasks', (req, res) => {
|
|
154
|
+
app.get('/api/growth/tasks', async (req, res) => {
|
|
152
155
|
const user = auth(req, res);
|
|
153
156
|
if (!user)
|
|
154
157
|
return;
|
|
155
158
|
const lang = String(req.headers['accept-language'] || '').startsWith('en') ? 'en' : 'zh';
|
|
156
|
-
res.json(evaluateGrowthTasks(db, user.id, lang));
|
|
159
|
+
res.json(await evaluateGrowthTasks(db, user.id, lang));
|
|
157
160
|
});
|
|
158
|
-
app.post('/api/growth/tasks/:id/claim', (req, res) => {
|
|
161
|
+
app.post('/api/growth/tasks/:id/claim', async (req, res) => {
|
|
159
162
|
const user = auth(req, res);
|
|
160
163
|
if (!user)
|
|
161
164
|
return;
|
|
162
165
|
const taskId = req.params.id;
|
|
163
166
|
if (!GROWTH_TASK_CATALOG.find(t => t.id === taskId))
|
|
164
167
|
return void res.json({ error: 'unknown task' });
|
|
165
|
-
const existing =
|
|
168
|
+
const existing = await dbOne("SELECT status, completed_at FROM growth_task_log WHERE user_id = ? AND task_id = ?", [user.id, taskId]);
|
|
166
169
|
if (existing?.status === 'completed')
|
|
167
170
|
return void res.json({ error: '该任务已完成' });
|
|
168
|
-
|
|
169
|
-
VALUES (?,?,?,datetime('now'),NULL)
|
|
171
|
+
await dbRun(`INSERT OR REPLACE INTO growth_task_log (user_id, task_id, status, claimed_at, completed_at)
|
|
172
|
+
VALUES (?,?,?,datetime('now'),NULL)`, [user.id, taskId, 'claimed']);
|
|
170
173
|
res.json({ success: true, status: 'claimed' });
|
|
171
174
|
});
|
|
172
|
-
app.post('/api/growth/tasks/:id/skip', (req, res) => {
|
|
175
|
+
app.post('/api/growth/tasks/:id/skip', async (req, res) => {
|
|
173
176
|
const user = auth(req, res);
|
|
174
177
|
if (!user)
|
|
175
178
|
return;
|
|
176
179
|
const taskId = req.params.id;
|
|
177
180
|
if (!GROWTH_TASK_CATALOG.find(t => t.id === taskId))
|
|
178
181
|
return void res.json({ error: 'unknown task' });
|
|
179
|
-
const existing =
|
|
182
|
+
const existing = await dbOne("SELECT status FROM growth_task_log WHERE user_id = ? AND task_id = ?", [user.id, taskId]);
|
|
180
183
|
if (existing?.status === 'completed')
|
|
181
184
|
return void res.json({ error: '该任务已完成,无法跳过' });
|
|
182
|
-
|
|
183
|
-
VALUES (?,?,?,NULL,NULL)
|
|
185
|
+
await dbRun(`INSERT OR REPLACE INTO growth_task_log (user_id, task_id, status, claimed_at, completed_at)
|
|
186
|
+
VALUES (?,?,?,NULL,NULL)`, [user.id, taskId, 'skipped']);
|
|
184
187
|
res.json({ success: true, status: 'skipped' });
|
|
185
188
|
});
|
|
186
|
-
app.post('/api/growth/tasks/:id/reset', (req, res) => {
|
|
189
|
+
app.post('/api/growth/tasks/:id/reset', async (req, res) => {
|
|
187
190
|
const user = auth(req, res);
|
|
188
191
|
if (!user)
|
|
189
192
|
return;
|
|
190
193
|
const taskId = req.params.id;
|
|
191
194
|
if (!GROWTH_TASK_CATALOG.find(t => t.id === taskId))
|
|
192
195
|
return void res.json({ error: 'unknown task' });
|
|
193
|
-
const existing =
|
|
196
|
+
const existing = await dbOne("SELECT status FROM growth_task_log WHERE user_id = ? AND task_id = ?", [user.id, taskId]);
|
|
194
197
|
if (existing?.status === 'completed')
|
|
195
198
|
return void res.json({ error: '已完成任务无法重置' });
|
|
196
|
-
|
|
199
|
+
await dbRun("DELETE FROM growth_task_log WHERE user_id = ? AND task_id = ?", [user.id, taskId]);
|
|
197
200
|
res.json({ success: true, status: 'available' });
|
|
198
201
|
});
|
|
199
202
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
export function registerImportProductRoutes(app, deps) {
|
|
2
|
-
|
|
3
|
+
// db 已走 RFC-016 异步 seam(dbOne/dbAll/dbRun),不再直接用 deps.db
|
|
4
|
+
const { auth, safeFetch, rateLimitOk, generateId, checkSellerCanList, anthropic, AnthropicCtor, FREE_IMPORT_LIMIT } = deps;
|
|
3
5
|
app.post('/api/import-product', async (req, res) => {
|
|
4
6
|
const user = auth(req, res);
|
|
5
7
|
if (!user)
|
|
@@ -14,19 +16,19 @@ export function registerImportProductRoutes(app, deps) {
|
|
|
14
16
|
return void res.json({ error: '请提供商品链接' });
|
|
15
17
|
if (!rateLimitOk(req.ip || 'unknown', 6, 60_000))
|
|
16
18
|
return void res.status(429).json({ error: '请求过于频繁,请稍后再试' });
|
|
17
|
-
const selfClaim =
|
|
19
|
+
const selfClaim = await dbOne(`
|
|
18
20
|
SELECT p.id as product_id, p.title FROM product_external_links pel
|
|
19
21
|
JOIN products p ON pel.product_id = p.id
|
|
20
22
|
WHERE pel.url = ? AND p.seller_id = ?
|
|
21
|
-
|
|
23
|
+
`, [url, user.id]);
|
|
22
24
|
if (selfClaim) {
|
|
23
25
|
return void res.json({ error: `您已上架过来自此链接的商品「${selfClaim.title}」,不能重复关联相同外部链接` });
|
|
24
26
|
}
|
|
25
|
-
const otherClaim =
|
|
27
|
+
const otherClaim = await dbOne(`
|
|
26
28
|
SELECT p.id as product_id FROM product_external_links pel
|
|
27
29
|
JOIN products p ON pel.product_id = p.id
|
|
28
30
|
WHERE pel.url = ? AND pel.verified = 1 AND p.seller_id != ?
|
|
29
|
-
|
|
31
|
+
`, [url, user.id]);
|
|
30
32
|
if (otherClaim) {
|
|
31
33
|
return void res.json({
|
|
32
34
|
conflict: true,
|
|
@@ -36,7 +38,7 @@ export function registerImportProductRoutes(app, deps) {
|
|
|
36
38
|
}
|
|
37
39
|
const usingOwnKey = typeof user_api_key === 'string' && user_api_key.trim().startsWith('sk-ant-');
|
|
38
40
|
if (!usingOwnKey) {
|
|
39
|
-
const todayCount =
|
|
41
|
+
const todayCount = (await dbOne(`SELECT COUNT(*) as cnt FROM import_logs WHERE user_id = ? AND created_at >= datetime('now', '-1 day')`, [user.id])).cnt;
|
|
40
42
|
if (todayCount >= FREE_IMPORT_LIMIT) {
|
|
41
43
|
return void res.json({
|
|
42
44
|
error: `今日免费导入次数已用完(${FREE_IMPORT_LIMIT} 次/天)。请在导入面板填入你自己的 Anthropic API Key 以继续使用。`,
|
|
@@ -67,10 +69,10 @@ export function registerImportProductRoutes(app, deps) {
|
|
|
67
69
|
return void res.json({ error: '链接指向私网/localhost 或经 redirect 触达内部地址,已拦截' });
|
|
68
70
|
return void res.json({ error: `无法访问该链接:${msg}` });
|
|
69
71
|
}
|
|
70
|
-
const avgPrices =
|
|
72
|
+
const avgPrices = await dbAll(`
|
|
71
73
|
SELECT category, AVG(price) as avg_price, MIN(price) as min_price, MAX(price) as max_price, COUNT(*) as cnt
|
|
72
74
|
FROM products WHERE status = 'active' GROUP BY category
|
|
73
|
-
`)
|
|
75
|
+
`);
|
|
74
76
|
const priceContext = avgPrices.map(r => `${r.category || '未分类'}:均价 ${r.avg_price?.toFixed(0)} WAZ,最低 ${r.min_price} WAZ,最高 ${r.max_price} WAZ(${r.cnt} 件商品)`).join('\n');
|
|
75
77
|
const client = usingOwnKey
|
|
76
78
|
? new AnthropicCtor({ apiKey: user_api_key.trim() })
|
|
@@ -138,9 +140,9 @@ ${html}`,
|
|
|
138
140
|
extracted.description = title;
|
|
139
141
|
}
|
|
140
142
|
if (!usingOwnKey) {
|
|
141
|
-
|
|
143
|
+
await dbRun(`INSERT INTO import_logs (id, user_id) VALUES (?, ?)`, [generateId('iml'), user.id]);
|
|
142
144
|
}
|
|
143
|
-
const usedToday = usingOwnKey ? 0 :
|
|
145
|
+
const usedToday = usingOwnKey ? 0 : (await dbOne(`SELECT COUNT(*) as cnt FROM import_logs WHERE user_id = ? AND created_at >= datetime('now', '-1 day')`, [user.id])).cnt;
|
|
144
146
|
res.json({
|
|
145
147
|
success: true,
|
|
146
148
|
source_url: url,
|
package/dist/pwa/routes/kyc.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { createHash } from 'crypto';
|
|
2
|
+
import { dbOne, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
2
3
|
export function registerKycRoutes(app, deps) {
|
|
3
|
-
|
|
4
|
+
// db 已走 RFC-016 异步 seam(dbOne/dbRun),不再直接用 deps.db
|
|
5
|
+
const { auth, MASTER_SEED } = deps;
|
|
4
6
|
const VALID_KYC_ID_TYPES = new Set(['passport', 'national_id', 'driver_license', 'other']);
|
|
5
|
-
app.post('/api/kyc/submit', (req, res) => {
|
|
7
|
+
app.post('/api/kyc/submit', async (req, res) => {
|
|
6
8
|
const user = auth(req, res);
|
|
7
9
|
if (!user)
|
|
8
10
|
return;
|
|
@@ -17,24 +19,23 @@ export function registerKycRoutes(app, deps) {
|
|
|
17
19
|
const idHash = createHash('sha256').update(idStr + MASTER_SEED).digest('hex');
|
|
18
20
|
const idLast4 = idStr.slice(-4);
|
|
19
21
|
// 已存在?必须先 admin reject 或允许重新提交
|
|
20
|
-
const existing =
|
|
22
|
+
const existing = await dbOne('SELECT status FROM kyc_records WHERE user_id = ?', [user.id]);
|
|
21
23
|
if (existing && existing.status === 'approved')
|
|
22
24
|
return void res.status(400).json({ error: '已通过认证,无需重复提交' });
|
|
23
25
|
if (existing && existing.status === 'pending')
|
|
24
26
|
return void res.status(400).json({ error: '审核中,请耐心等待' });
|
|
25
|
-
|
|
27
|
+
await dbRun(`INSERT INTO kyc_records (user_id, real_name, id_type, id_number_hash, id_number_last4, status, submitted_at)
|
|
26
28
|
VALUES (?,?,?,?,?,'pending', datetime('now'))
|
|
27
29
|
ON CONFLICT(user_id) DO UPDATE SET real_name = excluded.real_name, id_type = excluded.id_type,
|
|
28
30
|
id_number_hash = excluded.id_number_hash, id_number_last4 = excluded.id_number_last4,
|
|
29
|
-
status = 'pending', reject_reason = NULL, submitted_at = datetime('now')
|
|
30
|
-
.run(user.id, String(real_name).trim().slice(0, 60), String(id_type), idHash, idLast4);
|
|
31
|
+
status = 'pending', reject_reason = NULL, submitted_at = datetime('now')`, [user.id, String(real_name).trim().slice(0, 60), String(id_type), idHash, idLast4]);
|
|
31
32
|
res.json({ success: true, status: 'pending' });
|
|
32
33
|
});
|
|
33
|
-
app.get('/api/kyc/me', (req, res) => {
|
|
34
|
+
app.get('/api/kyc/me', async (req, res) => {
|
|
34
35
|
const user = auth(req, res);
|
|
35
36
|
if (!user)
|
|
36
37
|
return;
|
|
37
|
-
const row =
|
|
38
|
+
const row = await dbOne('SELECT status, id_type, id_number_last4, reject_reason, submitted_at, reviewed_at FROM kyc_records WHERE user_id = ?', [user.id]);
|
|
38
39
|
res.json({ kyc: row || null });
|
|
39
40
|
});
|
|
40
41
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
export function registerLeaderboardRoutes(app, deps) {
|
|
2
|
-
|
|
3
|
+
// db 已走 RFC-016 异步 seam(dbAll),不再直接用 deps.db
|
|
4
|
+
const { internalAuditorId, rateLimitOk } = deps;
|
|
3
5
|
const LB_RATE = 60; // 每 IP/分钟 60 次 — 公开端点防 DoS
|
|
4
|
-
app.get('/api/leaderboard', (req, res) => {
|
|
6
|
+
app.get('/api/leaderboard', async (req, res) => {
|
|
5
7
|
const ip = req.ip || 'unknown';
|
|
6
8
|
if (!rateLimitOk(`lb:${ip}`, LB_RATE, 60_000))
|
|
7
9
|
return void res.status(429).json({ error: 'rate-limited' });
|
|
@@ -10,7 +12,7 @@ export function registerLeaderboardRoutes(app, deps) {
|
|
|
10
12
|
if (kind === 'products') {
|
|
11
13
|
// recommend_count 严格语义:完成购买 + 4 星+评价 + 去重 buyer_id(一买家计 1)
|
|
12
14
|
// 排序权重也用 recommend_count 替代旧 unique_sharer_count(任何人分享)
|
|
13
|
-
const rows =
|
|
15
|
+
const rows = await dbAll(`
|
|
14
16
|
SELECT p.id, p.title, p.price, p.total_likes, p.completion_count,
|
|
15
17
|
u.handle as seller_handle, u.name as seller_name,
|
|
16
18
|
(SELECT COUNT(DISTINCT buyer_id) FROM order_ratings r
|
|
@@ -24,12 +26,12 @@ export function registerLeaderboardRoutes(app, deps) {
|
|
|
24
26
|
WHERE p.status = 'active' AND p.stock > 0
|
|
25
27
|
ORDER BY rank_score DESC, p.id DESC
|
|
26
28
|
LIMIT ?
|
|
27
|
-
|
|
29
|
+
`, [limit]);
|
|
28
30
|
return void res.json({ kind, items: rows });
|
|
29
31
|
}
|
|
30
32
|
if (kind === 'creators') {
|
|
31
33
|
// 创作者维度:聚合自己 shareables 的总点赞 + 关联商品总数
|
|
32
|
-
const rows =
|
|
34
|
+
const rows = await dbAll(`
|
|
33
35
|
SELECT u.id, u.handle, u.name, u.region,
|
|
34
36
|
COUNT(DISTINCT s.related_product_id) as products_shared,
|
|
35
37
|
COUNT(s.id) as shareable_count,
|
|
@@ -40,24 +42,24 @@ export function registerLeaderboardRoutes(app, deps) {
|
|
|
40
42
|
GROUP BY u.id
|
|
41
43
|
ORDER BY total_likes DESC, shareable_count DESC, u.id DESC
|
|
42
44
|
LIMIT ?
|
|
43
|
-
|
|
45
|
+
`, [limit]);
|
|
44
46
|
return void res.json({ kind, items: rows });
|
|
45
47
|
}
|
|
46
48
|
// B-2: 用户排行 — top buyers / sellers / verifiers
|
|
47
49
|
// 2026-05-23 隐私第一原理:移除 gmv 字段(运营状态私密,防过早 fork)
|
|
48
50
|
if (kind === 'buyers') {
|
|
49
|
-
const rows =
|
|
51
|
+
const rows = await dbAll(`
|
|
50
52
|
SELECT u.id, u.handle, u.name, u.region,
|
|
51
53
|
COUNT(*) as orders_count
|
|
52
54
|
FROM orders o JOIN users u ON u.id = o.buyer_id
|
|
53
55
|
WHERE o.status = 'completed' AND u.id NOT IN ('sys_protocol', ?)
|
|
54
56
|
GROUP BY u.id ORDER BY orders_count DESC, u.id DESC LIMIT ?
|
|
55
|
-
|
|
57
|
+
`, [internalAuditorId, limit]);
|
|
56
58
|
return void res.json({ kind, items: rows });
|
|
57
59
|
}
|
|
58
60
|
if (kind === 'sellers') {
|
|
59
61
|
// 排序改为 评分主导(avg_rating × log(1+rating_count)),不再按 GMV
|
|
60
|
-
const rows =
|
|
62
|
+
const rows = await dbAll(`
|
|
61
63
|
SELECT u.id, u.handle, u.name, u.region,
|
|
62
64
|
COUNT(*) as orders_count,
|
|
63
65
|
(SELECT COALESCE(AVG(stars), 0) FROM order_ratings WHERE seller_id = u.id) as avg_rating,
|
|
@@ -67,13 +69,13 @@ export function registerLeaderboardRoutes(app, deps) {
|
|
|
67
69
|
GROUP BY u.id
|
|
68
70
|
ORDER BY (avg_rating * (1.0 + log(1.0 + rating_count))) DESC, rating_count DESC, orders_count DESC
|
|
69
71
|
LIMIT ?
|
|
70
|
-
|
|
72
|
+
`, [limit]);
|
|
71
73
|
return void res.json({ kind, items: rows });
|
|
72
74
|
}
|
|
73
75
|
if (kind === 'value_products') {
|
|
74
76
|
// 2026-05-23 S5:极致性价比榜 — 按 value_badge=1 + 同类目 rank 排
|
|
75
77
|
// 排序:rank 越小越靠前(同类目第 1 名最便宜),相同 rank 按 pct 折扣大优先
|
|
76
|
-
const rows =
|
|
78
|
+
const rows = await dbAll(`
|
|
77
79
|
SELECT p.id, p.title, p.price, p.category,
|
|
78
80
|
p.value_badge_rank, p.value_badge_pct, p.value_badge_at,
|
|
79
81
|
p.completion_count, p.total_likes,
|
|
@@ -82,14 +84,14 @@ export function registerLeaderboardRoutes(app, deps) {
|
|
|
82
84
|
LEFT JOIN users u ON u.id = p.seller_id
|
|
83
85
|
WHERE p.value_badge = 1 AND p.status = 'active' AND p.stock > 0
|
|
84
86
|
ORDER BY p.value_badge_rank ASC, p.value_badge_pct DESC LIMIT ?
|
|
85
|
-
|
|
87
|
+
`, [limit]);
|
|
86
88
|
return void res.json({ kind, items: rows });
|
|
87
89
|
}
|
|
88
90
|
if (kind === 'agents') {
|
|
89
91
|
// 2026-05-22 AG1:Agent 评测竞赛榜单
|
|
90
92
|
// 数据源:agent_reputation(trust_score + level)+ agent_call_log(30d 调用数)
|
|
91
93
|
// 不暴露 api_key(隐私),只展示 user handle + 聚合指标
|
|
92
|
-
const rows =
|
|
94
|
+
const rows = await dbAll(`
|
|
93
95
|
SELECT u.id, u.handle, u.name,
|
|
94
96
|
MAX(ar.trust_score) as trust_score,
|
|
95
97
|
MAX(ar.level) as level,
|
|
@@ -103,7 +105,7 @@ export function registerLeaderboardRoutes(app, deps) {
|
|
|
103
105
|
GROUP BY u.id
|
|
104
106
|
HAVING calls_30d > 0 OR trust_score > 0
|
|
105
107
|
ORDER BY trust_score DESC, calls_30d DESC LIMIT ?
|
|
106
|
-
|
|
108
|
+
`, [limit]);
|
|
107
109
|
return void res.json({ kind, items: rows });
|
|
108
110
|
}
|
|
109
111
|
if (kind === 'arbitrators') {
|
|
@@ -114,7 +116,7 @@ export function registerLeaderboardRoutes(app, deps) {
|
|
|
114
116
|
// 2026-06-03 #1080 audit: ORDER BY 改为 case_count desc + u.id tie-breaker
|
|
115
117
|
// 移除 fairness_score 作为 secondary sort key — spec §3 禁 composite/multi-key
|
|
116
118
|
// ("display 4 separate dimensions, let user pick sort dimension")
|
|
117
|
-
const rows =
|
|
119
|
+
const rows = await dbAll(`
|
|
118
120
|
SELECT u.id, u.handle, u.name,
|
|
119
121
|
COUNT(dc.id) as cases_count,
|
|
120
122
|
COALESCE(SUM(dc.fairness_yes), 0) as total_yes,
|
|
@@ -129,7 +131,7 @@ export function registerLeaderboardRoutes(app, deps) {
|
|
|
129
131
|
WHERE dc.arbitrator_id IS NOT NULL
|
|
130
132
|
GROUP BY u.id
|
|
131
133
|
ORDER BY cases_count DESC, u.id DESC LIMIT ?
|
|
132
|
-
|
|
134
|
+
`, [limit]);
|
|
133
135
|
return void res.json({ kind, items: rows });
|
|
134
136
|
}
|
|
135
137
|
if (kind === 'verifiers') {
|
|
@@ -139,7 +141,7 @@ export function registerLeaderboardRoutes(app, deps) {
|
|
|
139
141
|
// 2026-06-03 #1080 audit: ORDER BY 改为 tasks_done desc(spec default case_count desc)
|
|
140
142
|
// + u.id tie-breaker。移除 tasks_correct/accuracy 作为 secondary sort key — 该排序奖励
|
|
141
143
|
// "活跃 + 准确" 隐含 composite,spec §3 明确"最活跃 first ≠ 最好 first"。
|
|
142
|
-
const rows =
|
|
144
|
+
const rows = await dbAll(`
|
|
143
145
|
SELECT u.id, u.handle, u.name,
|
|
144
146
|
vs.tasks_done, vs.tasks_correct, vs.tasks_wrong,
|
|
145
147
|
CASE WHEN vs.tasks_done > 0 THEN ROUND(CAST(vs.tasks_correct AS REAL) / vs.tasks_done, 3) ELSE NULL END as accuracy,
|
|
@@ -149,7 +151,7 @@ export function registerLeaderboardRoutes(app, deps) {
|
|
|
149
151
|
LEFT JOIN verifier_whitelist vw ON vw.user_id = vs.user_id
|
|
150
152
|
WHERE vs.tasks_done >= 1
|
|
151
153
|
ORDER BY vs.tasks_done DESC, u.id DESC LIMIT ?
|
|
152
|
-
|
|
154
|
+
`, [limit]);
|
|
153
155
|
return void res.json({ kind, items: rows });
|
|
154
156
|
}
|
|
155
157
|
return void res.json({ error: 'kind 必须是 products / creators / buyers / sellers / verifiers / arbitrators / agents / value_products' });
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
const URGENCY_WEIGHTS = {
|
|
2
3
|
now: { price: 0.10, eta: 0.50, trust: 0.20, region: 0.10, fresh: 0.10, eta_hard_max: 4 },
|
|
3
4
|
today: { price: 0.25, eta: 0.35, trust: 0.15, region: 0.15, fresh: 0.10, eta_hard_max: 24 },
|
|
@@ -28,12 +29,12 @@ function computeOfferScore(o, urgency, ctx) {
|
|
|
28
29
|
}
|
|
29
30
|
export function registerListingsRoutes(app, deps) {
|
|
30
31
|
const { db, generateId, auth, LISTING_CATEGORIES, BASE_LISTING_STAKE, VALID_FULFILLMENT_TYPES, isListingCategoryKey } = deps;
|
|
31
|
-
function sellerCompletedSales(uid) {
|
|
32
|
-
const r =
|
|
32
|
+
async function sellerCompletedSales(uid) {
|
|
33
|
+
const r = await dbOne(`SELECT COUNT(1) as n FROM orders WHERE seller_id = ? AND status = 'completed'`, [uid]);
|
|
33
34
|
return Number(r?.n ?? 0);
|
|
34
35
|
}
|
|
35
36
|
// 列表搜索(公开)
|
|
36
|
-
app.get('/api/listings', (req, res) => {
|
|
37
|
+
app.get('/api/listings', async (req, res) => {
|
|
37
38
|
const q = String(req.query.q || '').trim();
|
|
38
39
|
const category = String(req.query.category || '').trim();
|
|
39
40
|
const limit = Math.min(100, Math.max(1, Number(req.query.limit) || 30));
|
|
@@ -50,7 +51,7 @@ export function registerListingsRoutes(app, deps) {
|
|
|
50
51
|
args.push(category);
|
|
51
52
|
}
|
|
52
53
|
const orderBy = sort === 'popular' ? 'l.total_sales DESC, l.created_at DESC' : 'l.created_at DESC';
|
|
53
|
-
const rows =
|
|
54
|
+
const rows = await dbAll(`
|
|
54
55
|
SELECT l.*,
|
|
55
56
|
(SELECT MIN(p.price) FROM products p WHERE p.listing_id = l.id AND p.status = 'active') as min_price,
|
|
56
57
|
(SELECT COUNT(1) FROM products p WHERE p.listing_id = l.id AND p.status = 'active') as offer_count
|
|
@@ -58,17 +59,17 @@ export function registerListingsRoutes(app, deps) {
|
|
|
58
59
|
WHERE ${where.join(' AND ')}
|
|
59
60
|
ORDER BY ${orderBy}
|
|
60
61
|
LIMIT ?
|
|
61
|
-
|
|
62
|
+
`, [...args, limit]);
|
|
62
63
|
res.json({ items: rows, categories: LISTING_CATEGORIES });
|
|
63
64
|
});
|
|
64
65
|
// 我的跟卖
|
|
65
|
-
app.get('/api/listings/mine', (req, res) => {
|
|
66
|
+
app.get('/api/listings/mine', async (req, res) => {
|
|
66
67
|
const user = auth(req, res);
|
|
67
68
|
if (!user)
|
|
68
69
|
return;
|
|
69
70
|
if (user.role !== 'seller')
|
|
70
71
|
return void res.status(403).json({ error: '仅卖家可用', error_code: 'SELLER_ONLY' });
|
|
71
|
-
const rows =
|
|
72
|
+
const rows = await dbAll(`
|
|
72
73
|
SELECT l.id, l.title, l.category, l.category_path, l.external_id, l.created_at,
|
|
73
74
|
(SELECT COUNT(*) FROM products WHERE listing_id = l.id AND seller_id = ? AND status = 'active') as my_offer_count,
|
|
74
75
|
(SELECT MIN(price) FROM products WHERE listing_id = l.id AND seller_id = ? AND status = 'active') as my_min_price,
|
|
@@ -81,18 +82,18 @@ export function registerListingsRoutes(app, deps) {
|
|
|
81
82
|
)
|
|
82
83
|
ORDER BY l.created_at DESC
|
|
83
84
|
LIMIT 100
|
|
84
|
-
|
|
85
|
+
`, [user.id, user.id, user.id, user.id]);
|
|
85
86
|
res.json({ items: rows });
|
|
86
87
|
});
|
|
87
88
|
// 详情 + offers 加权排序
|
|
88
|
-
app.get('/api/listings/:id', (req, res) => {
|
|
89
|
-
const listing =
|
|
89
|
+
app.get('/api/listings/:id', async (req, res) => {
|
|
90
|
+
const listing = await dbOne("SELECT * FROM listings WHERE id = ? AND status != 'blocked'", [req.params.id]);
|
|
90
91
|
if (!listing)
|
|
91
92
|
return void res.status(404).json({ error: 'listing 不存在' });
|
|
92
93
|
const urgency = isUrgencyKey(String(req.query.urgency || '')) ? String(req.query.urgency) : 'flex';
|
|
93
94
|
const sortParam = String(req.query.sort || 'smart');
|
|
94
95
|
const sortMode = VALID_OFFER_SORTS.has(sortParam) ? sortParam : 'smart';
|
|
95
|
-
const offers =
|
|
96
|
+
const offers = await dbAll(`
|
|
96
97
|
SELECT p.id, p.seller_id, p.title, p.price, p.stock, p.status,
|
|
97
98
|
p.fulfillment_type, p.eta_hours, p.freshness_ts, p.is_clearance, p.clearance_until,
|
|
98
99
|
p.cold_start_remaining, p.listing_stake_locked, p.ship_regions, p.commission_rate,
|
|
@@ -103,7 +104,7 @@ export function registerListingsRoutes(app, deps) {
|
|
|
103
104
|
FROM products p
|
|
104
105
|
LEFT JOIN users u ON u.id = p.seller_id
|
|
105
106
|
WHERE p.listing_id = ? AND p.status = 'active'
|
|
106
|
-
|
|
107
|
+
`, [req.params.id]);
|
|
107
108
|
const buyerRegion = req.query.buyer_region ? String(req.query.buyer_region) : null;
|
|
108
109
|
const nowIso = new Date().toISOString();
|
|
109
110
|
if (offers.length) {
|
|
@@ -165,7 +166,7 @@ export function registerListingsRoutes(app, deps) {
|
|
|
165
166
|
res.json({ listing, offers, urgency, sort: sortMode, categories: LISTING_CATEGORIES });
|
|
166
167
|
});
|
|
167
168
|
// 创建 listing(首创者)
|
|
168
|
-
app.post('/api/listings', (req, res) => {
|
|
169
|
+
app.post('/api/listings', async (req, res) => {
|
|
169
170
|
const user = auth(req, res);
|
|
170
171
|
if (!user)
|
|
171
172
|
return;
|
|
@@ -178,26 +179,26 @@ export function registerListingsRoutes(app, deps) {
|
|
|
178
179
|
return void res.json({ error: '类目无效' });
|
|
179
180
|
const catCfg = LISTING_CATEGORIES[cat];
|
|
180
181
|
if (catCfg.requires_kyc) {
|
|
181
|
-
const k =
|
|
182
|
+
const k = await dbOne("SELECT status FROM kyc_records WHERE user_id = ?", [user.id]);
|
|
182
183
|
if (!k || k.status !== 'approved') {
|
|
183
184
|
return void res.json({ error: `${catCfg.name} 类目需先完成实名认证(KYC)`, error_code: 'KYC_REQUIRED' });
|
|
184
185
|
}
|
|
185
186
|
}
|
|
186
187
|
if (catCfg.min_sales > 0) {
|
|
187
|
-
const sales = sellerCompletedSales(user.id);
|
|
188
|
+
const sales = await sellerCompletedSales(user.id);
|
|
188
189
|
if (sales < catCfg.min_sales) {
|
|
189
190
|
return void res.json({ error: `${catCfg.name} 类目需至少 ${catCfg.min_sales} 单成功历史(当前 ${sales})` });
|
|
190
191
|
}
|
|
191
192
|
}
|
|
192
193
|
// 首创者 stake = 1.5 × 基础 × 类目倍数
|
|
193
194
|
const stakeRequired = Math.round(BASE_LISTING_STAKE * catCfg.stake_mult * 1.5 * 100) / 100;
|
|
194
|
-
const wallet =
|
|
195
|
+
const wallet = await dbOne('SELECT balance FROM wallets WHERE user_id = ?', [user.id]);
|
|
195
196
|
if (!wallet || Number(wallet.balance) < stakeRequired) {
|
|
196
197
|
return void res.json({ error: `余额不足,创建 ${catCfg.name} listing 需 ${stakeRequired} WAZ` });
|
|
197
198
|
}
|
|
198
199
|
const externalId = body.external_id ? String(body.external_id).trim() : null;
|
|
199
200
|
if (externalId) {
|
|
200
|
-
const existing =
|
|
201
|
+
const existing = await dbOne("SELECT id FROM listings WHERE external_id = ? AND status = 'active'", [externalId]);
|
|
201
202
|
if (existing)
|
|
202
203
|
return void res.json({ error: '该型号已存在 listing,请改为跟卖', listing_id: existing.id, suggestion: 'follow' });
|
|
203
204
|
}
|
|
@@ -218,19 +219,19 @@ export function registerListingsRoutes(app, deps) {
|
|
|
218
219
|
res.json({ id, stake_locked: stakeRequired, category: cat });
|
|
219
220
|
});
|
|
220
221
|
// 跟卖:为已有 listing 创建本卖家的 product(即一个 offer)
|
|
221
|
-
app.post('/api/listings/:id/offers', (req, res) => {
|
|
222
|
+
app.post('/api/listings/:id/offers', async (req, res) => {
|
|
222
223
|
const user = auth(req, res);
|
|
223
224
|
if (!user)
|
|
224
225
|
return;
|
|
225
226
|
if (user.role !== 'seller')
|
|
226
227
|
return void res.json({ error: '仅卖家可跟卖' });
|
|
227
|
-
const listing =
|
|
228
|
+
const listing = await dbOne("SELECT id, category, title, cover_image, description FROM listings WHERE id = ? AND status = 'active'", [req.params.id]);
|
|
228
229
|
if (!listing)
|
|
229
230
|
return void res.status(404).json({ error: 'listing 不存在或已下架' });
|
|
230
231
|
const cat = String(listing.category);
|
|
231
232
|
const catCfg = isListingCategoryKey(cat) ? LISTING_CATEGORIES[cat] : LISTING_CATEGORIES.general;
|
|
232
233
|
if (catCfg.min_sales > 0) {
|
|
233
|
-
const sales = sellerCompletedSales(user.id);
|
|
234
|
+
const sales = await sellerCompletedSales(user.id);
|
|
234
235
|
if (sales < catCfg.min_sales) {
|
|
235
236
|
return void res.json({ error: `${catCfg.name} 类目跟卖需至少 ${catCfg.min_sales} 单成功历史(当前 ${sales})` });
|
|
236
237
|
}
|
|
@@ -247,12 +248,12 @@ export function registerListingsRoutes(app, deps) {
|
|
|
247
248
|
return void res.json({ error: 'fulfillment_type 无效' });
|
|
248
249
|
const shipRegions = body.ship_regions ? String(body.ship_regions).trim() : '全国';
|
|
249
250
|
const stakeRequired = Math.round(BASE_LISTING_STAKE * catCfg.stake_mult * 100) / 100;
|
|
250
|
-
const wallet =
|
|
251
|
+
const wallet = await dbOne('SELECT balance FROM wallets WHERE user_id = ?', [user.id]);
|
|
251
252
|
if (!wallet || Number(wallet.balance) < stakeRequired) {
|
|
252
253
|
return void res.json({ error: `余额不足,跟卖 ${catCfg.name} 需 ${stakeRequired} WAZ` });
|
|
253
254
|
}
|
|
254
255
|
// 一卖家 × 一 listing = 一 offer
|
|
255
|
-
const existing =
|
|
256
|
+
const existing = await dbOne("SELECT id, status FROM products WHERE listing_id = ? AND seller_id = ? AND status != 'deleted'", [req.params.id, user.id]);
|
|
256
257
|
if (existing)
|
|
257
258
|
return void res.json({ error: '已存在该商品的 offer,请修改而非新建', offer_id: existing.id });
|
|
258
259
|
const id = generateId('p');
|
|
@@ -1,16 +1,18 @@
|
|
|
1
|
+
import { dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
export function registerLogisticsRoutes(app, deps) {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
// db 已走 RFC-016 异步 seam(dbAll),不再直接用 deps.db
|
|
4
|
+
const { auth } = deps;
|
|
5
|
+
app.get('/api/logistics/companies', async (_req, res) => {
|
|
6
|
+
const companies = await dbAll(`SELECT id, name FROM users WHERE role = 'logistics' ORDER BY name ASC`);
|
|
5
7
|
res.json(companies);
|
|
6
8
|
});
|
|
7
|
-
app.get('/api/logistics/orders', (req, res) => {
|
|
9
|
+
app.get('/api/logistics/orders', async (req, res) => {
|
|
8
10
|
const user = auth(req, res);
|
|
9
11
|
if (!user)
|
|
10
12
|
return;
|
|
11
13
|
if (user.role !== 'logistics')
|
|
12
14
|
return void res.status(403).json({ error: '仅限物流角色' });
|
|
13
|
-
const available =
|
|
15
|
+
const available = await dbAll(`
|
|
14
16
|
SELECT o.*, p.title as product_title, p.category,
|
|
15
17
|
ub.name as buyer_name, us.name as seller_name
|
|
16
18
|
FROM orders o
|
|
@@ -19,8 +21,8 @@ export function registerLogisticsRoutes(app, deps) {
|
|
|
19
21
|
JOIN users us ON o.seller_id = us.id
|
|
20
22
|
WHERE o.status = 'shipped' AND (o.logistics_id IS NULL OR o.logistics_id = '')
|
|
21
23
|
ORDER BY o.created_at ASC LIMIT 20
|
|
22
|
-
`)
|
|
23
|
-
const mine =
|
|
24
|
+
`);
|
|
25
|
+
const mine = await dbAll(`
|
|
24
26
|
SELECT o.*, p.title as product_title, p.category,
|
|
25
27
|
ub.name as buyer_name, us.name as seller_name
|
|
26
28
|
FROM orders o
|
|
@@ -29,7 +31,7 @@ export function registerLogisticsRoutes(app, deps) {
|
|
|
29
31
|
JOIN users us ON o.seller_id = us.id
|
|
30
32
|
WHERE o.logistics_id = ? AND o.status IN ('shipped','picked_up','in_transit')
|
|
31
33
|
ORDER BY o.created_at ASC LIMIT 20
|
|
32
|
-
|
|
34
|
+
`, [user.id]);
|
|
33
35
|
res.json({ available, mine });
|
|
34
36
|
});
|
|
35
37
|
}
|