@seasonkoh/webaz 0.1.7 → 0.1.9

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 (153) hide show
  1. package/LICENSE +48 -0
  2. package/README.md +156 -20
  3. package/dist/layer0-foundation/L0-1-database/schema.js +5 -4
  4. package/dist/layer0-foundation/L0-2-state-machine/engine.js +228 -7
  5. package/dist/layer0-foundation/L0-2-state-machine/order-chain.js +156 -0
  6. package/dist/layer0-foundation/L0-2-state-machine/transitions.js +53 -12
  7. package/dist/layer0-foundation/L0-5-manifest/manifest.js +14 -1
  8. package/dist/layer1-agent/L1-1-mcp-server/auth.js +1 -1
  9. package/dist/layer1-agent/L1-1-mcp-server/server.js +3691 -714
  10. package/dist/layer1-agent/L1-2-external-anchor/anchor-engine.js +324 -0
  11. package/dist/layer1-agent/L1-2-identity/agent-passport.js +100 -0
  12. package/dist/layer2-business/L2-6-notifications/notification-engine.js +72 -5
  13. package/dist/layer2-business/L2-7-snf/snf-engine.js +287 -0
  14. package/dist/layer2-business/L2-anchor-registry/anchor-registry.js +396 -0
  15. package/dist/layer2-business/L2-notes/note-photo-storage.js +133 -0
  16. package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +6 -6
  17. package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +246 -0
  18. package/dist/layer4-economics/L4-3-reputation/reputation-engine.js +95 -1
  19. package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +31 -2
  20. package/dist/layer4-economics/L4-4-skill-market/skill-listing-engine.js +358 -0
  21. package/dist/pwa/public/app.js +31947 -0
  22. package/dist/pwa/public/i18n.js +5751 -0
  23. package/dist/pwa/public/icon.svg +11 -0
  24. package/dist/pwa/public/index.html +21 -0
  25. package/dist/pwa/public/manifest.json +48 -0
  26. package/dist/pwa/public/openapi.json +5946 -0
  27. package/dist/pwa/public/style.css +535 -0
  28. package/dist/pwa/public/sw.js +63 -0
  29. package/dist/pwa/public/vendor/jsQR.js +10102 -0
  30. package/dist/pwa/public/webaz-logo.png +0 -0
  31. package/dist/pwa/routes/account-deletion.js +53 -0
  32. package/dist/pwa/routes/addresses.js +105 -0
  33. package/dist/pwa/routes/admin-admins.js +151 -0
  34. package/dist/pwa/routes/admin-analytics.js +253 -0
  35. package/dist/pwa/routes/admin-atomic.js +21 -0
  36. package/dist/pwa/routes/admin-catalog.js +64 -0
  37. package/dist/pwa/routes/admin-editor-picks.js +45 -0
  38. package/dist/pwa/routes/admin-events.js +60 -0
  39. package/dist/pwa/routes/admin-health.js +66 -0
  40. package/dist/pwa/routes/admin-moderation.js +120 -0
  41. package/dist/pwa/routes/admin-ops.js +179 -0
  42. package/dist/pwa/routes/admin-protocol-params.js +79 -0
  43. package/dist/pwa/routes/admin-reports.js +154 -0
  44. package/dist/pwa/routes/admin-tokenomics.js +113 -0
  45. package/dist/pwa/routes/admin-users-lifecycle.js +237 -0
  46. package/dist/pwa/routes/admin-users-query.js +390 -0
  47. package/dist/pwa/routes/admin-verifier-flow.js +126 -0
  48. package/dist/pwa/routes/admin-verifier-whitelist.js +111 -0
  49. package/dist/pwa/routes/admin-wallet-ops.js +66 -0
  50. package/dist/pwa/routes/agent-buy.js +215 -0
  51. package/dist/pwa/routes/agent-governance.js +341 -0
  52. package/dist/pwa/routes/agent-reputation.js +34 -0
  53. package/dist/pwa/routes/ai.js +101 -0
  54. package/dist/pwa/routes/analytics.js +272 -0
  55. package/dist/pwa/routes/anchors.js +169 -0
  56. package/dist/pwa/routes/announcements.js +110 -0
  57. package/dist/pwa/routes/arbitrator.js +117 -0
  58. package/dist/pwa/routes/auction.js +436 -0
  59. package/dist/pwa/routes/auth-login.js +40 -0
  60. package/dist/pwa/routes/auth-read.js +66 -0
  61. package/dist/pwa/routes/auth-register.js +138 -0
  62. package/dist/pwa/routes/auth-sessions.js +62 -0
  63. package/dist/pwa/routes/blocklist.js +60 -0
  64. package/dist/pwa/routes/buyer-feeds.js +224 -0
  65. package/dist/pwa/routes/cart.js +155 -0
  66. package/dist/pwa/routes/charity.js +816 -0
  67. package/dist/pwa/routes/chat.js +318 -0
  68. package/dist/pwa/routes/checkin-tasks.js +122 -0
  69. package/dist/pwa/routes/checkout-helpers.js +85 -0
  70. package/dist/pwa/routes/claim-initiators.js +88 -0
  71. package/dist/pwa/routes/claim-verify.js +615 -0
  72. package/dist/pwa/routes/claim-voting.js +114 -0
  73. package/dist/pwa/routes/claim-withdrawals.js +20 -0
  74. package/dist/pwa/routes/coupons.js +165 -0
  75. package/dist/pwa/routes/dashboards.js +99 -0
  76. package/dist/pwa/routes/dispute-cases.js +267 -0
  77. package/dist/pwa/routes/disputes-read.js +358 -0
  78. package/dist/pwa/routes/disputes-write.js +475 -0
  79. package/dist/pwa/routes/evidence.js +86 -0
  80. package/dist/pwa/routes/external-anchors.js +107 -0
  81. package/dist/pwa/routes/feedback.js +270 -0
  82. package/dist/pwa/routes/flash-sales.js +130 -0
  83. package/dist/pwa/routes/follows.js +103 -0
  84. package/dist/pwa/routes/group-buys.js +208 -0
  85. package/dist/pwa/routes/growth.js +199 -0
  86. package/dist/pwa/routes/import-product.js +153 -0
  87. package/dist/pwa/routes/kyc.js +40 -0
  88. package/dist/pwa/routes/leaderboard.js +149 -0
  89. package/dist/pwa/routes/listings.js +281 -0
  90. package/dist/pwa/routes/logistics.js +35 -0
  91. package/dist/pwa/routes/manifests.js +126 -0
  92. package/dist/pwa/routes/me-data.js +101 -0
  93. package/dist/pwa/routes/notifications.js +48 -0
  94. package/dist/pwa/routes/offers.js +96 -0
  95. package/dist/pwa/routes/orders-action.js +285 -0
  96. package/dist/pwa/routes/orders-create.js +339 -0
  97. package/dist/pwa/routes/orders-read.js +180 -0
  98. package/dist/pwa/routes/p2p-products.js +178 -0
  99. package/dist/pwa/routes/payments-governance.js +311 -0
  100. package/dist/pwa/routes/peers.js +34 -0
  101. package/dist/pwa/routes/pin-receipts.js +39 -0
  102. package/dist/pwa/routes/products-aliases.js +119 -0
  103. package/dist/pwa/routes/products-claims.js +60 -0
  104. package/dist/pwa/routes/products-create.js +206 -0
  105. package/dist/pwa/routes/products-crud.js +73 -0
  106. package/dist/pwa/routes/products-links.js +129 -0
  107. package/dist/pwa/routes/products-list.js +424 -0
  108. package/dist/pwa/routes/products-meta.js +155 -0
  109. package/dist/pwa/routes/products-update.js +125 -0
  110. package/dist/pwa/routes/profile-credentials.js +105 -0
  111. package/dist/pwa/routes/profile-identity.js +174 -0
  112. package/dist/pwa/routes/profile-location.js +35 -0
  113. package/dist/pwa/routes/profile-placement.js +70 -0
  114. package/dist/pwa/routes/profile-prefs.js +93 -0
  115. package/dist/pwa/routes/promoter.js +208 -0
  116. package/dist/pwa/routes/public-utils.js +170 -0
  117. package/dist/pwa/routes/push.js +54 -0
  118. package/dist/pwa/routes/ratings.js +220 -0
  119. package/dist/pwa/routes/recover-key.js +100 -0
  120. package/dist/pwa/routes/referral.js +58 -0
  121. package/dist/pwa/routes/reputation.js +34 -0
  122. package/dist/pwa/routes/returns.js +493 -0
  123. package/dist/pwa/routes/reviews.js +81 -0
  124. package/dist/pwa/routes/rfqs.js +443 -0
  125. package/dist/pwa/routes/search.js +172 -0
  126. package/dist/pwa/routes/secondhand.js +278 -0
  127. package/dist/pwa/routes/seller-quota.js +225 -0
  128. package/dist/pwa/routes/share-redirects.js +164 -0
  129. package/dist/pwa/routes/shareables-interactions.js +212 -0
  130. package/dist/pwa/routes/shareables.js +470 -0
  131. package/dist/pwa/routes/shops.js +98 -0
  132. package/dist/pwa/routes/signaling.js +43 -0
  133. package/dist/pwa/routes/skill-market.js +173 -0
  134. package/dist/pwa/routes/skills.js +174 -0
  135. package/dist/pwa/routes/snf.js +126 -0
  136. package/dist/pwa/routes/tags.js +47 -0
  137. package/dist/pwa/routes/trial.js +333 -0
  138. package/dist/pwa/routes/trusted-kpi.js +87 -0
  139. package/dist/pwa/routes/url-claim.js +113 -0
  140. package/dist/pwa/routes/users-public.js +317 -0
  141. package/dist/pwa/routes/variants.js +156 -0
  142. package/dist/pwa/routes/verifier-user.js +107 -0
  143. package/dist/pwa/routes/verify-tasks.js +120 -0
  144. package/dist/pwa/routes/waitlist.js +65 -0
  145. package/dist/pwa/routes/wallet-read.js +218 -0
  146. package/dist/pwa/routes/wallet-write.js +273 -0
  147. package/dist/pwa/routes/webauthn.js +188 -0
  148. package/dist/pwa/routes/webhooks.js +162 -0
  149. package/dist/pwa/routes/welcome.js +226 -0
  150. package/dist/pwa/routes/wishlist-qa.js +135 -0
  151. package/dist/pwa/security/ssrf.js +110 -0
  152. package/dist/pwa/server.js +9679 -698
  153. package/package.json +11 -4
@@ -0,0 +1,149 @@
1
+ export function registerLeaderboardRoutes(app, deps) {
2
+ const { db, internalAuditorId, rateLimitOk } = deps;
3
+ const LB_RATE = 60; // 每 IP/分钟 60 次 — 公开端点防 DoS
4
+ app.get('/api/leaderboard', (req, res) => {
5
+ const ip = req.ip || 'unknown';
6
+ if (!rateLimitOk(`lb:${ip}`, LB_RATE, 60_000))
7
+ return void res.status(429).json({ error: 'rate-limited' });
8
+ const kind = String(req.query.kind || 'products');
9
+ const limit = Math.min(50, Math.max(5, Number(req.query.limit) || 20));
10
+ if (kind === 'products') {
11
+ // recommend_count 严格语义:完成购买 + 4 星+评价 + 去重 buyer_id(一买家计 1)
12
+ // 排序权重也用 recommend_count 替代旧 unique_sharer_count(任何人分享)
13
+ const rows = db.prepare(`
14
+ SELECT p.id, p.title, p.price, p.total_likes, p.completion_count,
15
+ u.handle as seller_handle, u.name as seller_name,
16
+ (SELECT COUNT(DISTINCT buyer_id) FROM order_ratings r
17
+ WHERE r.product_id = p.id AND r.stars >= 4) as recommend_count,
18
+ (COALESCE(p.completion_count, 0) * 0.5
19
+ + (SELECT COUNT(DISTINCT buyer_id) FROM order_ratings r
20
+ WHERE r.product_id = p.id AND r.stars >= 4) * 2.0
21
+ + COALESCE(p.total_likes, 0) * 1.0) as rank_score
22
+ FROM products p
23
+ LEFT JOIN users u ON u.id = p.seller_id
24
+ WHERE p.status = 'active' AND p.stock > 0
25
+ ORDER BY rank_score DESC, p.id DESC
26
+ LIMIT ?
27
+ `).all(limit);
28
+ return void res.json({ kind, items: rows });
29
+ }
30
+ if (kind === 'creators') {
31
+ // 创作者维度:聚合自己 shareables 的总点赞 + 关联商品总数
32
+ const rows = db.prepare(`
33
+ SELECT u.id, u.handle, u.name, u.region,
34
+ COUNT(DISTINCT s.related_product_id) as products_shared,
35
+ COUNT(s.id) as shareable_count,
36
+ COALESCE(SUM(s.like_count), 0) as total_likes,
37
+ COALESCE(SUM(s.click_count), 0) as total_clicks
38
+ FROM users u
39
+ JOIN shareables s ON s.owner_id = u.id AND s.status = 'active'
40
+ GROUP BY u.id
41
+ ORDER BY total_likes DESC, shareable_count DESC, u.id DESC
42
+ LIMIT ?
43
+ `).all(limit);
44
+ return void res.json({ kind, items: rows });
45
+ }
46
+ // B-2: 用户排行 — top buyers / sellers / verifiers
47
+ // 2026-05-23 隐私第一原理:移除 gmv 字段(运营状态私密,防过早 fork)
48
+ if (kind === 'buyers') {
49
+ const rows = db.prepare(`
50
+ SELECT u.id, u.handle, u.name, u.region,
51
+ COUNT(*) as orders_count
52
+ FROM orders o JOIN users u ON u.id = o.buyer_id
53
+ WHERE o.status = 'completed' AND u.id NOT IN ('sys_protocol', ?)
54
+ GROUP BY u.id ORDER BY orders_count DESC, u.id DESC LIMIT ?
55
+ `).all(internalAuditorId, limit);
56
+ return void res.json({ kind, items: rows });
57
+ }
58
+ if (kind === 'sellers') {
59
+ // 排序改为 评分主导(avg_rating × log(1+rating_count)),不再按 GMV
60
+ const rows = db.prepare(`
61
+ SELECT u.id, u.handle, u.name, u.region,
62
+ COUNT(*) as orders_count,
63
+ (SELECT COALESCE(AVG(stars), 0) FROM order_ratings WHERE seller_id = u.id) as avg_rating,
64
+ (SELECT COUNT(*) FROM order_ratings WHERE seller_id = u.id) as rating_count
65
+ FROM orders o JOIN users u ON u.id = o.seller_id
66
+ WHERE o.status = 'completed' AND u.role = 'seller'
67
+ GROUP BY u.id
68
+ ORDER BY (avg_rating * (1.0 + log(1.0 + rating_count))) DESC, rating_count DESC, orders_count DESC
69
+ LIMIT ?
70
+ `).all(limit);
71
+ return void res.json({ kind, items: rows });
72
+ }
73
+ if (kind === 'value_products') {
74
+ // 2026-05-23 S5:极致性价比榜 — 按 value_badge=1 + 同类目 rank 排
75
+ // 排序:rank 越小越靠前(同类目第 1 名最便宜),相同 rank 按 pct 折扣大优先
76
+ const rows = db.prepare(`
77
+ SELECT p.id, p.title, p.price, p.category,
78
+ p.value_badge_rank, p.value_badge_pct, p.value_badge_at,
79
+ p.completion_count, p.total_likes,
80
+ u.handle as seller_handle, u.name as seller_name
81
+ FROM products p
82
+ LEFT JOIN users u ON u.id = p.seller_id
83
+ WHERE p.value_badge = 1 AND p.status = 'active' AND p.stock > 0
84
+ ORDER BY p.value_badge_rank ASC, p.value_badge_pct DESC LIMIT ?
85
+ `).all(limit);
86
+ return void res.json({ kind, items: rows });
87
+ }
88
+ if (kind === 'agents') {
89
+ // 2026-05-22 AG1:Agent 评测竞赛榜单
90
+ // 数据源:agent_reputation(trust_score + level)+ agent_call_log(30d 调用数)
91
+ // 不暴露 api_key(隐私),只展示 user handle + 聚合指标
92
+ const rows = db.prepare(`
93
+ SELECT u.id, u.handle, u.name,
94
+ MAX(ar.trust_score) as trust_score,
95
+ MAX(ar.level) as level,
96
+ COUNT(DISTINCT ar.api_key) as keys_count,
97
+ (SELECT COUNT(*) FROM agent_call_log acl
98
+ WHERE acl.user_id = u.id
99
+ AND acl.created_at > datetime('now', '-30 days')) as calls_30d
100
+ FROM agent_reputation ar
101
+ JOIN users u ON u.id = ar.user_id
102
+ WHERE u.id != 'sys_protocol'
103
+ GROUP BY u.id
104
+ HAVING calls_30d > 0 OR trust_score > 0
105
+ ORDER BY trust_score DESC, calls_30d DESC LIMIT ?
106
+ `).all(limit);
107
+ return void res.json({ kind, items: rows });
108
+ }
109
+ if (kind === 'arbitrators') {
110
+ // 2026-05-22 A3:仲裁员声誉排行
111
+ // 从 dispute_cases 聚合 — 每个 arbitrator_id 的 case 数 + 公平评价
112
+ // fairness_score = fairness_yes / (fairness_yes + fairness_no)(仅在有评价时)
113
+ const rows = db.prepare(`
114
+ SELECT u.id, u.handle, u.name,
115
+ COUNT(dc.id) as cases_count,
116
+ COALESCE(SUM(dc.fairness_yes), 0) as total_yes,
117
+ COALESCE(SUM(dc.fairness_no), 0) as total_no,
118
+ CASE
119
+ WHEN COALESCE(SUM(dc.fairness_yes + dc.fairness_no), 0) > 0
120
+ THEN ROUND(CAST(SUM(dc.fairness_yes) AS REAL) / SUM(dc.fairness_yes + dc.fairness_no), 3)
121
+ ELSE NULL
122
+ END as fairness_score
123
+ FROM dispute_cases dc
124
+ JOIN users u ON u.id = dc.arbitrator_id
125
+ WHERE dc.arbitrator_id IS NOT NULL
126
+ GROUP BY u.id
127
+ ORDER BY cases_count DESC, fairness_score DESC LIMIT ?
128
+ `).all(limit);
129
+ return void res.json({ kind, items: rows });
130
+ }
131
+ if (kind === 'verifiers') {
132
+ // 2026-05-22 V1:移除 tasks_done >= 5 门槛 — 小协议早期阶段会卡死榜单
133
+ // 新人有 tasks_done < 5 时前端打 "新人" badge 区分(仍能看到自己排名)
134
+ const rows = db.prepare(`
135
+ SELECT u.id, u.handle, u.name,
136
+ vs.tasks_done, vs.tasks_correct, vs.tasks_wrong,
137
+ CASE WHEN vs.tasks_done > 0 THEN ROUND(CAST(vs.tasks_correct AS REAL) / vs.tasks_done, 3) ELSE NULL END as accuracy,
138
+ vw.tier
139
+ FROM verifier_stats vs
140
+ JOIN users u ON u.id = vs.user_id
141
+ LEFT JOIN verifier_whitelist vw ON vw.user_id = vs.user_id
142
+ WHERE vs.tasks_done >= 1
143
+ ORDER BY vs.tasks_correct DESC, accuracy DESC, vs.tasks_done DESC LIMIT ?
144
+ `).all(limit);
145
+ return void res.json({ kind, items: rows });
146
+ }
147
+ return void res.json({ error: 'kind 必须是 products / creators / buyers / sellers / verifiers / arbitrators / agents / value_products' });
148
+ });
149
+ }
@@ -0,0 +1,281 @@
1
+ const URGENCY_WEIGHTS = {
2
+ now: { price: 0.10, eta: 0.50, trust: 0.20, region: 0.10, fresh: 0.10, eta_hard_max: 4 },
3
+ today: { price: 0.25, eta: 0.35, trust: 0.15, region: 0.15, fresh: 0.10, eta_hard_max: 24 },
4
+ flex: { price: 0.45, eta: 0.10, trust: 0.20, region: 0.10, fresh: 0.15, eta_hard_max: null },
5
+ };
6
+ function isUrgencyKey(s) {
7
+ return s === 'now' || s === 'today' || s === 'flex';
8
+ }
9
+ const VALID_OFFER_SORTS = new Set(['smart', 'cheapest', 'fastest', 'trusted', 'nearest', 'clearance']);
10
+ function computeOfferScore(o, urgency, ctx) {
11
+ const w = URGENCY_WEIGHTS[urgency];
12
+ const price = Number(o.price);
13
+ const eta = o.eta_hours != null ? Number(o.eta_hours) : null;
14
+ if (w.eta_hard_max != null && eta != null && eta > w.eta_hard_max)
15
+ return -1;
16
+ const pSpread = ctx.maxPrice - ctx.minPrice;
17
+ const priceNorm = pSpread > 0 ? (ctx.maxPrice - price) / pSpread : 1;
18
+ const etaScore = eta != null ? Math.max(0, 1 - eta / 72) : Math.max(0, 1 - 48 / 72);
19
+ const trustNorm = Math.min(1, Number(o.seller_sales || 0) / 100);
20
+ const regionMatch = ctx.buyerRegion && o.seller_region === ctx.buyerRegion ? 1 : 0.3;
21
+ const freshTs = o.freshness_ts ? String(o.freshness_ts) : o.updated_at;
22
+ const ageH = freshTs ? (Date.parse(ctx.nowIso) - Date.parse(freshTs)) / 3600_000 : 0;
23
+ const freshScore = ageH < 24 ? 1 : ageH < 168 ? 0.85 : 0.5;
24
+ let score = w.price * priceNorm + w.eta * etaScore + w.trust * trustNorm + w.region * regionMatch + w.fresh * freshScore;
25
+ if (Number(o.cold_start_remaining || 0) > 0)
26
+ score *= 0.7;
27
+ return Math.round(score * 10000) / 10000;
28
+ }
29
+ export function registerListingsRoutes(app, deps) {
30
+ const { db, generateId, auth, LISTING_CATEGORIES, BASE_LISTING_STAKE, VALID_FULFILLMENT_TYPES, isListingCategoryKey } = deps;
31
+ function sellerCompletedSales(uid) {
32
+ const r = db.prepare(`SELECT COUNT(1) as n FROM orders WHERE seller_id = ? AND status = 'completed'`).get(uid);
33
+ return Number(r?.n ?? 0);
34
+ }
35
+ // 列表搜索(公开)
36
+ app.get('/api/listings', (req, res) => {
37
+ const q = String(req.query.q || '').trim();
38
+ const category = String(req.query.category || '').trim();
39
+ const limit = Math.min(100, Math.max(1, Number(req.query.limit) || 30));
40
+ const sort = String(req.query.sort || 'newest');
41
+ const where = ["l.status = 'active'"];
42
+ const args = [];
43
+ if (q) {
44
+ const qE = String(q).replace(/[\\%_]/g, '\\$&');
45
+ where.push("(l.title LIKE ? ESCAPE '\\' OR l.spec LIKE ? ESCAPE '\\' OR l.category_path LIKE ? ESCAPE '\\')");
46
+ args.push(`%${qE}%`, `%${qE}%`, `%${qE}%`);
47
+ }
48
+ if (category && isListingCategoryKey(category)) {
49
+ where.push("l.category = ?");
50
+ args.push(category);
51
+ }
52
+ const orderBy = sort === 'popular' ? 'l.total_sales DESC, l.created_at DESC' : 'l.created_at DESC';
53
+ const rows = db.prepare(`
54
+ SELECT l.*,
55
+ (SELECT MIN(p.price) FROM products p WHERE p.listing_id = l.id AND p.status = 'active') as min_price,
56
+ (SELECT COUNT(1) FROM products p WHERE p.listing_id = l.id AND p.status = 'active') as offer_count
57
+ FROM listings l
58
+ WHERE ${where.join(' AND ')}
59
+ ORDER BY ${orderBy}
60
+ LIMIT ?
61
+ `).all(...args, limit);
62
+ res.json({ items: rows, categories: LISTING_CATEGORIES });
63
+ });
64
+ // 我的跟卖
65
+ app.get('/api/listings/mine', (req, res) => {
66
+ const user = auth(req, res);
67
+ if (!user)
68
+ return;
69
+ if (user.role !== 'seller')
70
+ return void res.status(403).json({ error: '仅卖家可用', error_code: 'SELLER_ONLY' });
71
+ const rows = db.prepare(`
72
+ SELECT l.id, l.title, l.category, l.category_path, l.external_id, l.created_at,
73
+ (SELECT COUNT(*) FROM products WHERE listing_id = l.id AND seller_id = ? AND status = 'active') as my_offer_count,
74
+ (SELECT MIN(price) FROM products WHERE listing_id = l.id AND seller_id = ? AND status = 'active') as my_min_price,
75
+ (SELECT COUNT(*) FROM products WHERE listing_id = l.id AND status = 'active') as total_offer_count,
76
+ (SELECT MIN(price) FROM products WHERE listing_id = l.id AND status = 'active') as global_min_price,
77
+ (l.created_by = ?) as is_creator
78
+ FROM listings l
79
+ WHERE l.status = 'active' AND EXISTS (
80
+ SELECT 1 FROM products WHERE listing_id = l.id AND seller_id = ? AND status = 'active'
81
+ )
82
+ ORDER BY l.created_at DESC
83
+ LIMIT 100
84
+ `).all(user.id, user.id, user.id, user.id);
85
+ res.json({ items: rows });
86
+ });
87
+ // 详情 + offers 加权排序
88
+ app.get('/api/listings/:id', (req, res) => {
89
+ const listing = db.prepare("SELECT * FROM listings WHERE id = ? AND status != 'blocked'").get(req.params.id);
90
+ if (!listing)
91
+ return void res.status(404).json({ error: 'listing 不存在' });
92
+ const urgency = isUrgencyKey(String(req.query.urgency || '')) ? String(req.query.urgency) : 'flex';
93
+ const sortParam = String(req.query.sort || 'smart');
94
+ const sortMode = VALID_OFFER_SORTS.has(sortParam) ? sortParam : 'smart';
95
+ const offers = db.prepare(`
96
+ SELECT p.id, p.seller_id, p.title, p.price, p.stock, p.status,
97
+ p.fulfillment_type, p.eta_hours, p.freshness_ts, p.is_clearance, p.clearance_until,
98
+ p.cold_start_remaining, p.listing_stake_locked, p.ship_regions, p.commission_rate,
99
+ p.created_at, p.updated_at,
100
+ u.handle as seller_handle,
101
+ u.region as seller_region,
102
+ (SELECT COUNT(1) FROM orders WHERE seller_id = p.seller_id AND status = 'completed') as seller_sales
103
+ FROM products p
104
+ LEFT JOIN users u ON u.id = p.seller_id
105
+ WHERE p.listing_id = ? AND p.status = 'active'
106
+ `).all(req.params.id);
107
+ const buyerRegion = req.query.buyer_region ? String(req.query.buyer_region) : null;
108
+ const nowIso = new Date().toISOString();
109
+ if (offers.length) {
110
+ const prices = offers.map(o => Number(o.price));
111
+ const minPrice = Math.min(...prices);
112
+ const maxPrice = Math.max(...prices);
113
+ const etas = offers.filter(o => o.eta_hours != null).map(o => Number(o.eta_hours));
114
+ const minEta = etas.length ? Math.min(...etas) : null;
115
+ offers.forEach(o => {
116
+ const tags = [];
117
+ if (Number(o.price) === minPrice)
118
+ tags.push('cheapest');
119
+ if (minEta != null && o.eta_hours != null && Number(o.eta_hours) === minEta)
120
+ tags.push('fastest');
121
+ if (buyerRegion && o.seller_region === buyerRegion)
122
+ tags.push('nearest');
123
+ if (Number(o.seller_sales) >= 50)
124
+ tags.push('trusted');
125
+ if (o.is_clearance && (!o.clearance_until || String(o.clearance_until) > nowIso))
126
+ tags.push('clearance');
127
+ const freshTs = o.freshness_ts ? String(o.freshness_ts) : o.updated_at;
128
+ const ageH = freshTs ? (Date.parse(nowIso) - Date.parse(freshTs)) / 3600_000 : 0;
129
+ if (ageH >= 168)
130
+ tags.push('stale');
131
+ o.tags = tags;
132
+ o.score = computeOfferScore(o, urgency, { minPrice, maxPrice, buyerRegion, nowIso });
133
+ });
134
+ if (sortMode === 'smart') {
135
+ offers.sort((a, b) => Number(b.score) - Number(a.score));
136
+ }
137
+ else if (sortMode === 'cheapest') {
138
+ offers.sort((a, b) => Number(a.price) - Number(b.price));
139
+ }
140
+ else if (sortMode === 'fastest') {
141
+ offers.sort((a, b) => (Number(a.eta_hours ?? Infinity) - Number(b.eta_hours ?? Infinity)));
142
+ }
143
+ else if (sortMode === 'trusted') {
144
+ offers.sort((a, b) => Number(b.seller_sales || 0) - Number(a.seller_sales || 0));
145
+ }
146
+ else if (sortMode === 'nearest') {
147
+ offers.sort((a, b) => {
148
+ const am = buyerRegion && a.seller_region === buyerRegion ? 0 : 1;
149
+ const bm = buyerRegion && b.seller_region === buyerRegion ? 0 : 1;
150
+ if (am !== bm)
151
+ return am - bm;
152
+ return Number(a.price) - Number(b.price);
153
+ });
154
+ }
155
+ else if (sortMode === 'clearance') {
156
+ offers.sort((a, b) => {
157
+ const ac = a.is_clearance ? 0 : 1;
158
+ const bc = b.is_clearance ? 0 : 1;
159
+ if (ac !== bc)
160
+ return ac - bc;
161
+ return Number(a.price) - Number(b.price);
162
+ });
163
+ }
164
+ }
165
+ res.json({ listing, offers, urgency, sort: sortMode, categories: LISTING_CATEGORIES });
166
+ });
167
+ // 创建 listing(首创者)
168
+ app.post('/api/listings', (req, res) => {
169
+ const user = auth(req, res);
170
+ if (!user)
171
+ return;
172
+ const body = req.body;
173
+ const title = String(body.title || '').trim();
174
+ if (title.length < 2)
175
+ return void res.json({ error: 'title 至少 2 字' });
176
+ const cat = String(body.category || 'general');
177
+ if (!isListingCategoryKey(cat))
178
+ return void res.json({ error: '类目无效' });
179
+ const catCfg = LISTING_CATEGORIES[cat];
180
+ if (catCfg.requires_kyc) {
181
+ const k = db.prepare("SELECT status FROM kyc_records WHERE user_id = ?").get(user.id);
182
+ if (!k || k.status !== 'approved') {
183
+ return void res.json({ error: `${catCfg.name} 类目需先完成实名认证(KYC)`, error_code: 'KYC_REQUIRED' });
184
+ }
185
+ }
186
+ if (catCfg.min_sales > 0) {
187
+ const sales = sellerCompletedSales(user.id);
188
+ if (sales < catCfg.min_sales) {
189
+ return void res.json({ error: `${catCfg.name} 类目需至少 ${catCfg.min_sales} 单成功历史(当前 ${sales})` });
190
+ }
191
+ }
192
+ // 首创者 stake = 1.5 × 基础 × 类目倍数
193
+ const stakeRequired = Math.round(BASE_LISTING_STAKE * catCfg.stake_mult * 1.5 * 100) / 100;
194
+ const wallet = db.prepare('SELECT balance FROM wallets WHERE user_id = ?').get(user.id);
195
+ if (!wallet || Number(wallet.balance) < stakeRequired) {
196
+ return void res.json({ error: `余额不足,创建 ${catCfg.name} listing 需 ${stakeRequired} WAZ` });
197
+ }
198
+ const externalId = body.external_id ? String(body.external_id).trim() : null;
199
+ if (externalId) {
200
+ const existing = db.prepare("SELECT id FROM listings WHERE external_id = ? AND status = 'active'").get(externalId);
201
+ if (existing)
202
+ return void res.json({ error: '该型号已存在 listing,请改为跟卖', listing_id: existing.id, suggestion: 'follow' });
203
+ }
204
+ const id = generateId('l');
205
+ const tx = db.transaction(() => {
206
+ db.prepare(`
207
+ INSERT INTO listings (id, external_id, category, category_path, title, spec, cover_image, description, created_by)
208
+ VALUES (?,?,?,?,?,?,?,?,?)
209
+ `).run(id, externalId, cat, body.category_path ? String(body.category_path) : null, title, body.spec ? JSON.stringify(body.spec) : null, body.cover_image ? String(body.cover_image) : null, body.description ? String(body.description) : null, user.id);
210
+ db.prepare(`UPDATE wallets SET balance = balance - ?, staked = staked + ? WHERE user_id = ?`).run(stakeRequired, stakeRequired, user.id);
211
+ });
212
+ try {
213
+ tx();
214
+ }
215
+ catch (e) {
216
+ return void res.status(500).json({ error: String(e.message) });
217
+ }
218
+ res.json({ id, stake_locked: stakeRequired, category: cat });
219
+ });
220
+ // 跟卖:为已有 listing 创建本卖家的 product(即一个 offer)
221
+ app.post('/api/listings/:id/offers', (req, res) => {
222
+ const user = auth(req, res);
223
+ if (!user)
224
+ return;
225
+ if (user.role !== 'seller')
226
+ return void res.json({ error: '仅卖家可跟卖' });
227
+ const listing = db.prepare("SELECT id, category, title, cover_image, description FROM listings WHERE id = ? AND status = 'active'").get(req.params.id);
228
+ if (!listing)
229
+ return void res.status(404).json({ error: 'listing 不存在或已下架' });
230
+ const cat = String(listing.category);
231
+ const catCfg = isListingCategoryKey(cat) ? LISTING_CATEGORIES[cat] : LISTING_CATEGORIES.general;
232
+ if (catCfg.min_sales > 0) {
233
+ const sales = sellerCompletedSales(user.id);
234
+ if (sales < catCfg.min_sales) {
235
+ return void res.json({ error: `${catCfg.name} 类目跟卖需至少 ${catCfg.min_sales} 单成功历史(当前 ${sales})` });
236
+ }
237
+ }
238
+ const body = req.body;
239
+ const priceNum = Number(body.price);
240
+ if (!Number.isFinite(priceNum) || priceNum <= 0)
241
+ return void res.json({ error: 'price 必须 > 0' });
242
+ const stockNum = Math.max(0, Math.floor(Number(body.stock) || 0));
243
+ if (stockNum < 1)
244
+ return void res.json({ error: 'stock 至少 1' });
245
+ const fulfillmentType = String(body.fulfillment_type || 'standard');
246
+ if (!VALID_FULFILLMENT_TYPES.has(fulfillmentType))
247
+ return void res.json({ error: 'fulfillment_type 无效' });
248
+ const shipRegions = body.ship_regions ? String(body.ship_regions).trim() : '全国';
249
+ const stakeRequired = Math.round(BASE_LISTING_STAKE * catCfg.stake_mult * 100) / 100;
250
+ const wallet = db.prepare('SELECT balance FROM wallets WHERE user_id = ?').get(user.id);
251
+ if (!wallet || Number(wallet.balance) < stakeRequired) {
252
+ return void res.json({ error: `余额不足,跟卖 ${catCfg.name} 需 ${stakeRequired} WAZ` });
253
+ }
254
+ // 一卖家 × 一 listing = 一 offer
255
+ const existing = db.prepare("SELECT id, status FROM products WHERE listing_id = ? AND seller_id = ? AND status != 'deleted'").get(req.params.id, user.id);
256
+ if (existing)
257
+ return void res.json({ error: '已存在该商品的 offer,请修改而非新建', offer_id: existing.id });
258
+ const id = generateId('p');
259
+ const coverImg = body.cover_image ? String(body.cover_image) : listing.cover_image;
260
+ const imagesJson = coverImg ? JSON.stringify([coverImg]) : '[]';
261
+ const tx = db.transaction(() => {
262
+ db.prepare(`
263
+ INSERT INTO products (
264
+ id, seller_id, title, description, price, stock, status, images,
265
+ ship_regions, handling_hours, commission_rate, category_id, stake_amount,
266
+ listing_id, fulfillment_type, eta_hours, freshness_ts,
267
+ is_clearance, clearance_until, cold_start_remaining, listing_stake_locked
268
+ ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,datetime('now'),?,?,?,?)
269
+ `).run(id, user.id, listing.title, (body.description ? String(body.description) : listing.description || ''), priceNum, stockNum, 'active', imagesJson, shipRegions, 24, 0.10, 'cat_default', 0, req.params.id, fulfillmentType, body.eta_hours != null ? Number(body.eta_hours) : null, body.is_clearance ? 1 : 0, body.clearance_until ? String(body.clearance_until) : null, catCfg.cold_start, stakeRequired);
270
+ db.prepare(`UPDATE wallets SET balance = balance - ?, staked = staked + ? WHERE user_id = ?`).run(stakeRequired, stakeRequired, user.id);
271
+ db.prepare(`UPDATE listings SET total_offers = total_offers + 1, updated_at = datetime('now') WHERE id = ?`).run(req.params.id);
272
+ });
273
+ try {
274
+ tx();
275
+ }
276
+ catch (e) {
277
+ return void res.status(500).json({ error: String(e.message) });
278
+ }
279
+ res.json({ id, stake_locked: stakeRequired });
280
+ });
281
+ }
@@ -0,0 +1,35 @@
1
+ export function registerLogisticsRoutes(app, deps) {
2
+ const { db, auth } = deps;
3
+ app.get('/api/logistics/companies', (_req, res) => {
4
+ const companies = db.prepare(`SELECT id, name FROM users WHERE role = 'logistics' ORDER BY name ASC`).all();
5
+ res.json(companies);
6
+ });
7
+ app.get('/api/logistics/orders', (req, res) => {
8
+ const user = auth(req, res);
9
+ if (!user)
10
+ return;
11
+ if (user.role !== 'logistics')
12
+ return void res.status(403).json({ error: '仅限物流角色' });
13
+ const available = db.prepare(`
14
+ SELECT o.*, p.title as product_title, p.category,
15
+ ub.name as buyer_name, us.name as seller_name
16
+ FROM orders o
17
+ JOIN products p ON o.product_id = p.id
18
+ JOIN users ub ON o.buyer_id = ub.id
19
+ JOIN users us ON o.seller_id = us.id
20
+ WHERE o.status = 'shipped' AND (o.logistics_id IS NULL OR o.logistics_id = '')
21
+ ORDER BY o.created_at ASC LIMIT 20
22
+ `).all();
23
+ const mine = db.prepare(`
24
+ SELECT o.*, p.title as product_title, p.category,
25
+ ub.name as buyer_name, us.name as seller_name
26
+ FROM orders o
27
+ JOIN products p ON o.product_id = p.id
28
+ JOIN users ub ON o.buyer_id = ub.id
29
+ JOIN users us ON o.seller_id = us.id
30
+ WHERE o.logistics_id = ? AND o.status IN ('shipped','picked_up','in_transit')
31
+ ORDER BY o.created_at ASC LIMIT 20
32
+ `).all(user.id);
33
+ res.json({ available, mine });
34
+ });
35
+ }
@@ -0,0 +1,126 @@
1
+ import crypto from 'crypto';
2
+ const MANIFEST_DAILY_LIMIT = 20;
3
+ const THUMB_MAX_BYTES = 12000; // ~12KB base64 ≈ 9KB 原始图
4
+ function verifyManifestSig(hash, ownerId, contentType, byteSize, signedAt, apiKey, signature) {
5
+ const payload = `${hash}|${ownerId}|${contentType}|${byteSize}|${signedAt}`;
6
+ const expected = crypto.createHmac('sha256', apiKey).update(payload).digest('hex');
7
+ return expected === signature;
8
+ }
9
+ export function registerManifestsRoutes(app, deps) {
10
+ const { db, auth, safeRoles } = deps;
11
+ app.post('/api/manifests', (req, res) => {
12
+ const me = auth(req, res);
13
+ if (!me)
14
+ return;
15
+ const { hash, content_type, byte_size, title, description, thumbnail_data_uri, signature, signed_at, related_product_id, related_anchor } = req.body || {};
16
+ if (!hash || !/^[a-f0-9]{64}$/.test(hash))
17
+ return void res.json({ error: 'hash 必须为 64 字符十六进制' });
18
+ if (!content_type || !signature || !signed_at)
19
+ return void res.json({ error: '缺少必要字段' });
20
+ if (typeof byte_size !== 'number' || byte_size <= 0 || byte_size > 500 * 1024 * 1024)
21
+ return void res.json({ error: 'byte_size 不合法(最大 500MB)' });
22
+ if (!related_product_id && !related_anchor)
23
+ return void res.json({ error: '请关联商品或流量口令' });
24
+ if (thumbnail_data_uri && thumbnail_data_uri.length > THUMB_MAX_BYTES)
25
+ return void res.json({ error: '缩略图过大(≤ 9KB 原始)' });
26
+ // 验签
27
+ const apiKey = me.api_key;
28
+ if (!verifyManifestSig(hash, me.id, content_type, byte_size, signed_at, apiKey, signature)) {
29
+ return void res.json({ error: '签名验证失败' });
30
+ }
31
+ // 日上限
32
+ const todayCount = db.prepare(`SELECT COUNT(*) as n FROM manifest_registry WHERE owner_id = ? AND created_at > datetime('now', '-1 day')`).get(me.id).n;
33
+ if (todayCount >= MANIFEST_DAILY_LIMIT)
34
+ return void res.json({ error: `每日上限 ${MANIFEST_DAILY_LIMIT} 条` });
35
+ if (related_product_id) {
36
+ const p = db.prepare("SELECT id FROM products WHERE id = ?").get(related_product_id);
37
+ if (!p)
38
+ return void res.json({ error: '关联商品不存在' });
39
+ }
40
+ try {
41
+ db.prepare(`INSERT INTO manifest_registry (hash, owner_id, content_type, byte_size, title, description, thumbnail_data_uri, signature, signed_at, related_product_id, related_anchor)
42
+ VALUES (?,?,?,?,?,?,?,?,?,?,?)`)
43
+ .run(hash, me.id, content_type, byte_size, title || null, description || null, thumbnail_data_uri || null, signature, signed_at, related_product_id || null, related_anchor || null);
44
+ // 创作者立即注册为 owner peer
45
+ db.prepare(`INSERT OR REPLACE INTO peer_directory (peer_id, manifest_hash, is_owner, pin_intent, last_heartbeat)
46
+ VALUES (?,?,1,1,datetime('now'))`).run(me.id, hash);
47
+ res.json({ ok: true, hash });
48
+ }
49
+ catch (e) {
50
+ const msg = e?.message || '';
51
+ if (msg.includes('UNIQUE'))
52
+ return void res.json({ error: '该 hash 已注册', existing: true });
53
+ res.json({ error: '发布失败:' + msg });
54
+ }
55
+ });
56
+ app.get('/api/manifests/me', (req, res) => {
57
+ const me = auth(req, res);
58
+ if (!me)
59
+ return;
60
+ const rows = db.prepare(`
61
+ SELECT m.*, p.title as product_title FROM manifest_registry m
62
+ LEFT JOIN products p ON p.id = m.related_product_id
63
+ WHERE m.owner_id = ? AND m.status != 'removed'
64
+ ORDER BY m.created_at DESC LIMIT 100
65
+ `).all(me.id);
66
+ res.json({ manifests: rows });
67
+ });
68
+ app.get('/api/manifests/:hash', (req, res) => {
69
+ const me = auth(req, res);
70
+ if (!me)
71
+ return;
72
+ const m = db.prepare(`SELECT * FROM manifest_registry WHERE hash = ?`).get(req.params.hash);
73
+ if (!m)
74
+ return void res.status(404).json({ error: 'manifest 不存在' });
75
+ if (m.status === 'removed' || m.status === 'takedown_admin')
76
+ return void res.json({ error: '内容已下架', removed: true, reason: m.takedown_reason || null });
77
+ const peers = db.prepare(`
78
+ SELECT peer_id, is_owner, pin_intent, last_heartbeat FROM peer_directory
79
+ WHERE manifest_hash = ? AND last_heartbeat > datetime('now', '-5 minutes')
80
+ ORDER BY is_owner DESC, last_heartbeat DESC LIMIT 30
81
+ `).all(req.params.hash);
82
+ res.json({ manifest: m, peers });
83
+ });
84
+ app.get('/api/manifests/by-product/:pid', (req, res) => {
85
+ const user = auth(req, res);
86
+ if (!user)
87
+ return;
88
+ const rows = db.prepare(`
89
+ SELECT m.*, u.name as owner_name FROM manifest_registry m
90
+ LEFT JOIN users u ON u.id = m.owner_id
91
+ WHERE m.related_product_id = ? AND m.status = 'active'
92
+ ORDER BY m.created_at DESC LIMIT 20
93
+ `).all(req.params.pid);
94
+ res.json({ manifests: rows });
95
+ });
96
+ app.get('/api/manifests/by-anchor/:anchor', (req, res) => {
97
+ const user = auth(req, res);
98
+ if (!user)
99
+ return;
100
+ const rows = db.prepare(`
101
+ SELECT m.*, u.name as owner_name FROM manifest_registry m
102
+ LEFT JOIN users u ON u.id = m.owner_id
103
+ WHERE m.related_anchor = ? AND m.status = 'active'
104
+ ORDER BY m.created_at DESC LIMIT 50
105
+ `).all(req.params.anchor);
106
+ res.json({ manifests: rows });
107
+ });
108
+ app.patch('/api/manifests/:hash/takedown', (req, res) => {
109
+ const me = auth(req, res);
110
+ if (!me)
111
+ return;
112
+ const m = db.prepare("SELECT owner_id FROM manifest_registry WHERE hash = ?").get(req.params.hash);
113
+ if (!m)
114
+ return void res.status(404).json({ error: 'manifest 不存在' });
115
+ const isAdmin = me.role === 'admin' || safeRoles(me).includes('admin');
116
+ const isOwner = m.owner_id === me.id;
117
+ if (!isOwner && !isAdmin)
118
+ return void res.json({ error: '无权下架' });
119
+ const reason = (req.body?.reason || '').toString().slice(0, 200);
120
+ db.prepare(`UPDATE manifest_registry SET status = ?, takedown_reason = ?, takedown_at = datetime('now'), takedown_by = ? WHERE hash = ?`)
121
+ .run(isAdmin && !isOwner ? 'takedown_admin' : 'removed', reason, me.id, req.params.hash);
122
+ // 同步清空 peer directory(强制客户端 evict)
123
+ db.prepare("DELETE FROM peer_directory WHERE manifest_hash = ?").run(req.params.hash);
124
+ res.json({ ok: true });
125
+ });
126
+ }