@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,117 @@
1
+ export function registerArbitratorRoutes(app, deps) {
2
+ const { db, generateId, auth, requireArbitrationAdmin, checkArbitratorEligibility, getArbitratorState, errorRes, logAdminAction, ARB_STAKE_REQUIRED, ARB_APP_REJECT_COOLDOWN_DAYS, } = deps;
3
+ app.get('/api/arbitrator/eligibility', (req, res) => {
4
+ const user = auth(req, res);
5
+ if (!user)
6
+ return;
7
+ res.json(checkArbitratorEligibility(user.id));
8
+ });
9
+ app.get('/api/arbitrator/status', (req, res) => {
10
+ const user = auth(req, res);
11
+ if (!user)
12
+ return;
13
+ res.json(getArbitratorState(user.id));
14
+ });
15
+ app.post('/api/arbitrator/apply', (req, res) => {
16
+ const user = auth(req, res);
17
+ if (!user)
18
+ return;
19
+ if (user.role !== 'buyer') {
20
+ return void errorRes(res, 403, 'ROLE_NOT_BUYER', '外部仲裁员仅 buyer 角色可申请(卖家 / 受信角色请联系管理员)');
21
+ }
22
+ const userId = user.id;
23
+ const wl = db.prepare("SELECT 1 FROM arbitrator_whitelist WHERE user_id = ?").get(userId);
24
+ if (wl)
25
+ return void res.json({ error: '你已经是仲裁员,无需重新申请' });
26
+ const pending = db.prepare("SELECT 1 FROM arbitrator_applications WHERE user_id = ? AND status = 'pending'").get(userId);
27
+ if (pending)
28
+ return void res.json({ error: '你已有待审申请' });
29
+ const lastReject = db.prepare("SELECT reviewed_at FROM arbitrator_applications WHERE user_id = ? AND status = 'rejected' ORDER BY reviewed_at DESC LIMIT 1").get(userId);
30
+ if (lastReject?.reviewed_at) {
31
+ const cooldownEnd = new Date(new Date(lastReject.reviewed_at).getTime() + ARB_APP_REJECT_COOLDOWN_DAYS * 86400_000);
32
+ if (cooldownEnd > new Date()) {
33
+ return void res.json({ error: `申请冷却期未结束,可在 ${cooldownEnd.toISOString().slice(0, 10)} 后重新申请` });
34
+ }
35
+ }
36
+ const elig = checkArbitratorEligibility(userId);
37
+ if (!elig.eligible)
38
+ return void res.json({ error: '信誉指标未达标', eligibility: elig });
39
+ if (ARB_STAKE_REQUIRED > 0) {
40
+ const wallet = db.prepare("SELECT balance, staked FROM wallets WHERE user_id = ?").get(userId);
41
+ if (!wallet || wallet.balance < ARB_STAKE_REQUIRED) {
42
+ return void res.json({ error: `质押需 ${ARB_STAKE_REQUIRED} WAZ,钱包余额不足` });
43
+ }
44
+ db.prepare("UPDATE wallets SET balance = balance - ?, staked = staked + ? WHERE user_id = ?")
45
+ .run(ARB_STAKE_REQUIRED, ARB_STAKE_REQUIRED, userId);
46
+ }
47
+ db.prepare("INSERT INTO arbitrator_applications (id, user_id, status, snapshot) VALUES (?,?,?,?)")
48
+ .run(generateId('aapp'), userId, 'pending', JSON.stringify(elig.items));
49
+ res.json({ success: true, stake_locked: ARB_STAKE_REQUIRED });
50
+ });
51
+ app.post('/api/arbitrator/withdraw-application', (req, res) => {
52
+ const user = auth(req, res);
53
+ if (!user)
54
+ return;
55
+ const userId = user.id;
56
+ const pending = db.prepare("SELECT id FROM arbitrator_applications WHERE user_id = ? AND status = 'pending' LIMIT 1").get(userId);
57
+ if (!pending)
58
+ return void res.json({ error: '没有待审申请' });
59
+ db.prepare("UPDATE arbitrator_applications SET status='withdrawn', reviewed_at=datetime('now') WHERE id = ?").run(pending.id);
60
+ if (ARB_STAKE_REQUIRED > 0) {
61
+ db.prepare("UPDATE wallets SET balance = balance + ?, staked = staked - ? WHERE user_id = ?")
62
+ .run(ARB_STAKE_REQUIRED, ARB_STAKE_REQUIRED, userId);
63
+ }
64
+ res.json({ success: true });
65
+ });
66
+ // Admin
67
+ app.get('/api/admin/arbitrator-applications', (req, res) => {
68
+ const admin = requireArbitrationAdmin(req, res);
69
+ if (!admin)
70
+ return;
71
+ const status = String(req.query.status || 'pending');
72
+ const items = db.prepare(`
73
+ SELECT aa.*, u.name as user_name, u.handle, u.region
74
+ FROM arbitrator_applications aa
75
+ JOIN users u ON u.id = aa.user_id
76
+ WHERE aa.status = ?
77
+ ORDER BY aa.applied_at DESC LIMIT 100
78
+ `).all(status);
79
+ res.json({ items });
80
+ });
81
+ app.post('/api/admin/arbitrator-applications/:id/approve', (req, res) => {
82
+ const admin = requireArbitrationAdmin(req, res);
83
+ if (!admin)
84
+ return;
85
+ const { note } = req.body;
86
+ const appRow = db.prepare("SELECT id, user_id, status FROM arbitrator_applications WHERE id = ?").get(req.params.id);
87
+ if (!appRow)
88
+ return void res.json({ error: '申请不存在' });
89
+ if (appRow.status !== 'pending')
90
+ return void res.json({ error: '该申请不在待审状态' });
91
+ db.prepare("UPDATE arbitrator_applications SET status='approved', reviewed_at=datetime('now'), reviewed_by=?, decision_note=? WHERE id=?")
92
+ .run(admin.id, note || null, appRow.id);
93
+ db.prepare(`INSERT OR REPLACE INTO arbitrator_whitelist (user_id, note, is_system, granted_by, stake_amount) VALUES (?,?,0,?,?)`)
94
+ .run(appRow.user_id, note || '外部仲裁员批准', admin.id, ARB_STAKE_REQUIRED);
95
+ logAdminAction(admin.id, 'approve_arbitrator', 'user', appRow.user_id, { note });
96
+ res.json({ success: true });
97
+ });
98
+ app.post('/api/admin/arbitrator-applications/:id/reject', (req, res) => {
99
+ const admin = requireArbitrationAdmin(req, res);
100
+ if (!admin)
101
+ return;
102
+ const { note } = req.body;
103
+ const appRow = db.prepare("SELECT id, user_id, status FROM arbitrator_applications WHERE id = ?").get(req.params.id);
104
+ if (!appRow)
105
+ return void res.json({ error: '申请不存在' });
106
+ if (appRow.status !== 'pending')
107
+ return void res.json({ error: '该申请不在待审状态' });
108
+ db.prepare("UPDATE arbitrator_applications SET status='rejected', reviewed_at=datetime('now'), reviewed_by=?, decision_note=? WHERE id=?")
109
+ .run(admin.id, note || null, appRow.id);
110
+ if (ARB_STAKE_REQUIRED > 0) {
111
+ db.prepare("UPDATE wallets SET balance = balance + ?, staked = staked - ? WHERE user_id = ?")
112
+ .run(ARB_STAKE_REQUIRED, ARB_STAKE_REQUIRED, appRow.user_id);
113
+ }
114
+ logAdminAction(admin.id, 'reject_arbitrator', 'user', appRow.user_id, { note });
115
+ res.json({ success: true });
116
+ });
117
+ }
@@ -0,0 +1,436 @@
1
+ // ─── 拍卖常量(域内)──────────────────────────────────────────
2
+ const AUC_MAX_WINDOW_MIN = 14 * 24 * 60; // 14 天上限
3
+ const AUC_MIN_WINDOW_MIN = 5;
4
+ const AUC_DEFAULT_WINDOW_MIN = 60;
5
+ const AUC_DEFAULT_INCREMENT = 1;
6
+ const AUC_DEFAULT_SNIPER_MIN = 5;
7
+ const AUC_SELLER_STAKE_PCT = 0.05;
8
+ const AUC_BUYER_STAKE_PCT = 0.05;
9
+ const AUC_DAILY_CAP_PER_SELLER = 20;
10
+ function aucSellerStake(startingPrice) {
11
+ return Math.max(1, Math.round(startingPrice * AUC_SELLER_STAKE_PCT * 100) / 100);
12
+ }
13
+ function aucBuyerStake(price, qty) {
14
+ return Math.max(0.5, Math.round(price * qty * AUC_BUYER_STAKE_PCT * 100) / 100);
15
+ }
16
+ // 2026-05-24 #959:拍卖「⏰ 提醒我」3 个 endpoint
17
+ // 默认订阅 = deadline 前 60min + 10min 各 1 条通知
18
+ const AUCTION_REMINDER_LEADS = [60, 10];
19
+ // ─── 提醒 cron — 60s 扫一次,派发 due 提醒 + 标记 sent_at ────
20
+ export function fireDueAuctionReminders(db, generateId) {
21
+ const due = db.prepare(`
22
+ SELECT r.id, r.auction_id, r.user_id, r.lead_minutes,
23
+ a.title as auction_title, a.current_price, a.deadline_at, a.status as auction_status
24
+ FROM auction_reminders r
25
+ JOIN auctions a ON a.id = r.auction_id
26
+ WHERE r.sent_at IS NULL AND r.fire_at <= datetime('now')
27
+ LIMIT 200
28
+ `).all();
29
+ let fired = 0;
30
+ for (const r of due) {
31
+ try {
32
+ // 拍卖已不是 open(结束/取消)→ 跳过通知但标 sent_at 避免重复扫
33
+ if (r.auction_status !== 'open') {
34
+ db.prepare("UPDATE auction_reminders SET sent_at=datetime('now') WHERE id=?").run(r.id);
35
+ continue;
36
+ }
37
+ const title = `⏰ 拍卖${r.lead_minutes >= 60 ? Math.round(r.lead_minutes / 60) + 'h' : r.lead_minutes + 'min'}后结束`;
38
+ const body = `${r.auction_title} · 当前价 ${r.current_price} WAZ`;
39
+ const tx = db.transaction(() => {
40
+ db.prepare(`INSERT INTO notifications (id, user_id, type, title, body, actions)
41
+ VALUES (?, ?, 'auction_reminder', ?, ?, ?)`).run(generateId('ntf'), r.user_id, title, body, JSON.stringify([{ label: '去出价', hash: '#auction/' + r.auction_id }]));
42
+ db.prepare("UPDATE auction_reminders SET sent_at=datetime('now') WHERE id=?").run(r.id);
43
+ });
44
+ tx();
45
+ fired++;
46
+ }
47
+ catch (e) {
48
+ console.error('[cron auction-reminder]', r.id, e);
49
+ }
50
+ }
51
+ return { fired };
52
+ }
53
+ export function registerAuctionRoutes(app, deps) {
54
+ const { db, auth, generateId, RFQ_MAX_QTY, RFQ_MAX_PRICE, LISTING_CATEGORIES, isListingCategoryKey, requireProtocolAdmin } = deps;
55
+ // 卖家发起拍卖
56
+ app.post('/api/auctions', (req, res) => {
57
+ const user = auth(req, res);
58
+ if (!user)
59
+ return;
60
+ if (user.role !== 'seller')
61
+ return void res.json({ error: '仅卖家可发起拍卖' });
62
+ const body = req.body;
63
+ const title = String(body.title || '').trim();
64
+ if (title.length < 2)
65
+ return void res.json({ error: '标题至少 2 字' });
66
+ const qty = Math.max(1, Math.floor(Number(body.qty) || 1));
67
+ if (qty > RFQ_MAX_QTY)
68
+ return void res.json({ error: `qty 超出上限 ${RFQ_MAX_QTY}` });
69
+ const cat = String(body.category || 'general');
70
+ if (!isListingCategoryKey(cat))
71
+ return void res.json({ error: '类目无效' });
72
+ const startingPrice = Number(body.starting_price);
73
+ if (!Number.isFinite(startingPrice) || startingPrice <= 0)
74
+ return void res.json({ error: 'starting_price 必须 > 0' });
75
+ if (startingPrice > RFQ_MAX_PRICE)
76
+ return void res.json({ error: `starting_price 超出上限 ${RFQ_MAX_PRICE} WAZ` });
77
+ const minIncrement = Number(body.min_increment ?? AUC_DEFAULT_INCREMENT);
78
+ if (!Number.isFinite(minIncrement) || minIncrement <= 0)
79
+ return void res.json({ error: 'min_increment 必须 > 0' });
80
+ const reservePrice = body.reserve_price != null ? Number(body.reserve_price) : null;
81
+ if (reservePrice != null) {
82
+ if (!Number.isFinite(reservePrice) || reservePrice <= 0)
83
+ return void res.json({ error: 'reserve_price 无效' });
84
+ if (reservePrice < startingPrice)
85
+ return void res.json({ error: 'reserve_price 不可低于 starting_price' });
86
+ }
87
+ const windowMin = Math.max(AUC_MIN_WINDOW_MIN, Math.min(AUC_MAX_WINDOW_MIN, Math.floor(Number(body.window_min || AUC_DEFAULT_WINDOW_MIN))));
88
+ const sniperExtend = Math.max(0, Math.min(60, Math.floor(Number(body.sniper_extend_min ?? AUC_DEFAULT_SNIPER_MIN))));
89
+ // 频率限制
90
+ const today = db.prepare("SELECT COUNT(1) as n FROM auctions WHERE seller_id = ? AND created_at > datetime('now','-1 day')").get(user.id).n;
91
+ if (today >= AUC_DAILY_CAP_PER_SELLER)
92
+ return void res.json({ error: `今日已达上限 ${AUC_DAILY_CAP_PER_SELLER} 场拍卖` });
93
+ // 卖家担保金
94
+ const sellerStake = aucSellerStake(startingPrice);
95
+ const wallet = db.prepare('SELECT balance FROM wallets WHERE user_id = ?').get(user.id);
96
+ if (!wallet || Number(wallet.balance) < sellerStake) {
97
+ return void res.json({ error: `余额不足,卖家担保金 ${sellerStake} WAZ(5% × 起拍价)` });
98
+ }
99
+ // product_id 引用(可选):若提供,校验属于本人 + stock>=qty
100
+ let productId = null;
101
+ if (body.product_id) {
102
+ productId = String(body.product_id);
103
+ const p = db.prepare("SELECT seller_id, stock, status FROM products WHERE id = ?").get(productId);
104
+ if (!p)
105
+ return void res.json({ error: '关联商品不存在' });
106
+ if (p.seller_id !== user.id)
107
+ return void res.json({ error: '关联商品归属不匹配' });
108
+ if (p.status !== 'active')
109
+ return void res.json({ error: '关联商品未上架' });
110
+ if (Number(p.stock) < qty)
111
+ return void res.json({ error: `库存不足(${p.stock} < ${qty})` });
112
+ }
113
+ const id = generateId('auc');
114
+ db.transaction(() => {
115
+ db.prepare(`
116
+ INSERT INTO auctions (id, seller_id, listing_id, product_id, title, spec_json, qty, category,
117
+ starting_price, current_price, min_increment, reserve_price, deadline_at, sniper_extend_min,
118
+ seller_stake_locked, notes)
119
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,datetime('now', '+' || ? || ' minutes'),?,?,?)
120
+ `).run(id, user.id, body.listing_id ? String(body.listing_id) : null, productId, title, body.spec_json ? JSON.stringify(body.spec_json) : null, qty, cat, startingPrice, startingPrice, minIncrement, reservePrice, windowMin, sniperExtend, sellerStake, body.notes ? String(body.notes).slice(0, 500) : null);
121
+ db.prepare('UPDATE wallets SET balance = balance - ?, staked = staked + ? WHERE user_id = ?').run(sellerStake, sellerStake, user.id);
122
+ if (productId)
123
+ db.prepare("UPDATE products SET status = 'auction_pending', updated_at = datetime('now') WHERE id = ?").run(productId);
124
+ })();
125
+ // QA 轮 12 P1:返回完整 echo 字段 + ISO deadline_at 与 detail 一致
126
+ const created = db.prepare('SELECT deadline_at, status FROM auctions WHERE id = ?').get(id);
127
+ res.json({
128
+ id,
129
+ seller_stake: sellerStake,
130
+ window_min: windowMin,
131
+ deadline_at_minutes: windowMin, // 保留以兼容老客户端
132
+ deadline_at: created.deadline_at, // 与 detail 一致的 ISO
133
+ status: created.status,
134
+ starting_price: startingPrice,
135
+ current_price: startingPrice,
136
+ reserve_price: reservePrice,
137
+ min_increment: minIncrement,
138
+ sniper_extend_min: sniperExtend,
139
+ qty,
140
+ category: cat,
141
+ notes: body.notes ? String(body.notes).slice(0, 500) : null,
142
+ });
143
+ });
144
+ // 看板:浏览公开拍卖(匿名可访问)
145
+ app.get('/api/auctions', (req, res) => {
146
+ const where = ["a.status = 'open'", "a.deadline_at > datetime('now')"];
147
+ const args = [];
148
+ if (req.query.category) {
149
+ where.push('a.category = ?');
150
+ args.push(String(req.query.category));
151
+ }
152
+ if (req.query.q) {
153
+ const qE = String(req.query.q).replace(/[\\%_]/g, '\\$&');
154
+ where.push("(a.title LIKE ? ESCAPE '\\' OR a.notes LIKE ? ESCAPE '\\')");
155
+ const like = '%' + qE + '%';
156
+ args.push(like, like);
157
+ }
158
+ const limit = Math.min(100, Math.max(1, Number(req.query.limit) || 30));
159
+ const rows = db.prepare(`
160
+ SELECT a.id, a.seller_id, a.title, a.qty, a.category, a.starting_price, a.current_price,
161
+ a.min_increment, a.reserve_price, a.deadline_at, a.bid_count, a.sniper_extend_min, a.created_at,
162
+ u.handle as seller_handle, u.region as seller_region
163
+ FROM auctions a
164
+ LEFT JOIN users u ON u.id = a.seller_id
165
+ WHERE ${where.join(' AND ')}
166
+ ORDER BY a.deadline_at ASC
167
+ LIMIT ?
168
+ `).all(...args, limit);
169
+ res.json({ items: rows, categories: LISTING_CATEGORIES });
170
+ });
171
+ // 我的:买家=我出过价的,卖家=我发起的
172
+ app.get('/api/auctions/mine', (req, res) => {
173
+ const user = auth(req, res);
174
+ if (!user)
175
+ return;
176
+ const seller = db.prepare(`SELECT * FROM auctions WHERE seller_id = ? ORDER BY created_at DESC LIMIT 50`).all(user.id);
177
+ const buyer = db.prepare(`
178
+ SELECT DISTINCT a.*, (SELECT b.price FROM auction_bids b WHERE b.auction_id = a.id AND b.buyer_id = ? ORDER BY b.submitted_at DESC LIMIT 1) as my_last_bid,
179
+ (SELECT b.status FROM auction_bids b WHERE b.auction_id = a.id AND b.buyer_id = ? ORDER BY b.submitted_at DESC LIMIT 1) as my_last_status
180
+ FROM auctions a
181
+ JOIN auction_bids b ON b.auction_id = a.id
182
+ WHERE b.buyer_id = ? ORDER BY a.created_at DESC LIMIT 50
183
+ `).all(user.id, user.id, user.id);
184
+ res.json({ as_seller: seller, as_buyer: buyer });
185
+ });
186
+ // 详情:含 bid 历史(buyer 身份脱敏;卖家+出价人本人 可见全名)
187
+ app.get('/api/auctions/:id', (req, res) => {
188
+ const user = auth(req, res);
189
+ if (!user)
190
+ return;
191
+ const auc = db.prepare('SELECT * FROM auctions WHERE id = ?').get(req.params.id);
192
+ if (!auc)
193
+ return void res.status(404).json({ error: '拍卖不存在' });
194
+ const isSellerSelf = auc.seller_id === user.id;
195
+ const isSettled = auc.status !== 'open';
196
+ const bids = db.prepare(`
197
+ SELECT b.id, b.buyer_id, b.price, b.stake_locked, b.status, b.submitted_at, b.resolved_at,
198
+ u.handle as buyer_handle
199
+ FROM auction_bids b
200
+ LEFT JOIN users u ON u.id = b.buyer_id
201
+ WHERE b.auction_id = ?
202
+ ORDER BY b.price DESC, b.submitted_at ASC
203
+ `).all(req.params.id);
204
+ // 脱敏:非 (卖家/拍卖结束/出价人本人) 时,buyer_id 用后 6 位 + handle 隐藏
205
+ const safeBids = bids.map(b => {
206
+ const isMine = b.buyer_id === user.id;
207
+ if (isSellerSelf || isSettled || isMine)
208
+ return b;
209
+ return {
210
+ ...b,
211
+ buyer_id: '买家 #' + String(b.buyer_id || '').slice(-6),
212
+ buyer_handle: null,
213
+ };
214
+ });
215
+ res.json({ auction: auc, bids: safeBids, is_seller: isSellerSelf });
216
+ });
217
+ // 拍卖「⏰ 提醒我」(#959)
218
+ app.post('/api/auctions/:id/remind', (req, res) => {
219
+ const user = auth(req, res);
220
+ if (!user)
221
+ return;
222
+ const aucRow = db.prepare("SELECT id, deadline_at, status, seller_id FROM auctions WHERE id = ?").get(req.params.id);
223
+ if (!aucRow)
224
+ return void res.status(404).json({ error: '拍卖不存在' });
225
+ if (aucRow.seller_id === user.id)
226
+ return void res.status(400).json({ error: '卖家本人无需订阅自己的拍卖' });
227
+ if (aucRow.status !== 'open')
228
+ return void res.status(400).json({ error: '该拍卖已结束,无需提醒' });
229
+ // SQLite datetime('now') 是 UTC,但 JS Date 解析无 Z 的字符串当本地时间 — 强制按 UTC 解析
230
+ const deadlineMs = new Date(aucRow.deadline_at.replace(' ', 'T') + 'Z').getTime();
231
+ if (deadlineMs <= Date.now())
232
+ return void res.status(400).json({ error: '拍卖已截止' });
233
+ const tx = db.transaction(() => {
234
+ for (const lead of AUCTION_REMINDER_LEADS) {
235
+ const fireAtMs = deadlineMs - lead * 60_000;
236
+ if (fireAtMs <= Date.now())
237
+ continue; // 已过该提醒时间,跳过该 lead
238
+ const fireAtIso = new Date(fireAtMs).toISOString().replace('T', ' ').slice(0, 19);
239
+ db.prepare(`INSERT OR IGNORE INTO auction_reminders (id, auction_id, user_id, lead_minutes, fire_at)
240
+ VALUES (?, ?, ?, ?, ?)`).run(generateId('arm'), aucRow.id, user.id, lead, fireAtIso);
241
+ }
242
+ });
243
+ tx();
244
+ res.json({ ok: true, subscribed: true, leads_minutes: AUCTION_REMINDER_LEADS });
245
+ });
246
+ app.delete('/api/auctions/:id/remind', (req, res) => {
247
+ const user = auth(req, res);
248
+ if (!user)
249
+ return;
250
+ const r = db.prepare("DELETE FROM auction_reminders WHERE auction_id = ? AND user_id = ?").run(req.params.id, user.id);
251
+ res.json({ ok: true, deleted: r.changes });
252
+ });
253
+ app.get('/api/auctions/:id/remind', (req, res) => {
254
+ const user = auth(req, res);
255
+ if (!user)
256
+ return;
257
+ const rows = db.prepare("SELECT lead_minutes, fire_at, sent_at FROM auction_reminders WHERE auction_id = ? AND user_id = ? ORDER BY lead_minutes DESC").all(req.params.id, user.id);
258
+ res.json({ subscribed: rows.length > 0, reminders: rows });
259
+ });
260
+ // 买家:出价
261
+ app.post('/api/auctions/:id/bids', (req, res) => {
262
+ const user = auth(req, res);
263
+ if (!user)
264
+ return;
265
+ if (user.role !== 'buyer')
266
+ return void res.json({ error: '仅买家可出价' });
267
+ const auc = db.prepare('SELECT * FROM auctions WHERE id = ?').get(req.params.id);
268
+ if (!auc)
269
+ return void res.status(404).json({ error: '拍卖不存在' });
270
+ if (auc.status !== 'open')
271
+ return void res.json({ error: `当前状态 ${auc.status} 不接受出价` });
272
+ if (auc.seller_id === user.id)
273
+ return void res.json({ error: '卖家不能自拍自买' });
274
+ // deadline 校验(cron 可能未及时翻状态)
275
+ if (String(auc.deadline_at) <= new Date().toISOString().replace('T', ' ').slice(0, 19)) {
276
+ return void res.json({ error: '拍卖已到期,等待结算' });
277
+ }
278
+ const price = Number(req.body.price);
279
+ if (!Number.isFinite(price) || price <= 0)
280
+ return void res.json({ error: 'price 必须 > 0' });
281
+ if (price > RFQ_MAX_PRICE)
282
+ return void res.json({ error: `price 超出上限 ${RFQ_MAX_PRICE} WAZ` });
283
+ const minNextPrice = Math.round((Number(auc.current_price) + Number(auc.min_increment)) * 100) / 100;
284
+ const curPrice = Number(auc.current_price);
285
+ const startingPrice = Number(auc.starting_price);
286
+ // 首次出价:≥ starting_price;之后:≥ current_price + min_increment
287
+ const isFirst = Number(auc.bid_count) === 0;
288
+ if (isFirst) {
289
+ if (price < startingPrice)
290
+ return void res.json({ error: `首次出价不能低于起拍价 ${startingPrice}` });
291
+ }
292
+ else {
293
+ if (price < minNextPrice)
294
+ return void res.json({ error: `下一口价至少 ${minNextPrice}(当前 ${curPrice} + 加价 ${auc.min_increment})` });
295
+ }
296
+ const qty = Math.max(1, Math.floor(Number(auc.qty || 1)));
297
+ const stake = aucBuyerStake(price, qty);
298
+ const wallet = db.prepare('SELECT balance FROM wallets WHERE user_id = ?').get(user.id);
299
+ // QA 轮 12 P1:自我加价 affordability check 应含已锁旧 stake(会在 tx 内释放)
300
+ // 否则用户必须 ≥ 2× stake 余额才能加价,UX 卡。
301
+ const myExisting = db.prepare("SELECT stake_locked FROM auction_bids WHERE auction_id = ? AND buyer_id = ? AND status = 'active'").get(req.params.id, user.id);
302
+ const myExistingStake = Number(myExisting?.stake_locked || 0);
303
+ const availableForBid = Number(wallet?.balance || 0) + myExistingStake;
304
+ if (!wallet || availableForBid < stake) {
305
+ return void res.json({ error: `余额不足,出价押金 ${stake} WAZ(被超越后立即释放)` });
306
+ }
307
+ // 上一个最高 active bid(如果是别人的)→ outbid + 释放 stake
308
+ // 自己的旧 active bid(同 auction 同 buyer)→ outbid + 释放 stake
309
+ const id = generateId('abid');
310
+ let newDeadlineExt = null;
311
+ let sellerTopup = 0;
312
+ let closedErr = '';
313
+ db.transaction(() => {
314
+ // P1 #4:TX 内重读 auction 状态 + deadline 防 TOCTOU
315
+ const fresh = db.prepare('SELECT status, deadline_at, current_price, bid_count, seller_stake_locked, max_extends, extends_used, sniper_extend_min FROM auctions WHERE id = ?').get(req.params.id);
316
+ if (!fresh) {
317
+ closedErr = 'not_found';
318
+ return;
319
+ }
320
+ if (fresh.status !== 'open') {
321
+ closedErr = `closed_${fresh.status}`;
322
+ return;
323
+ }
324
+ if (fresh.deadline_at <= new Date().toISOString().replace('T', ' ').slice(0, 19)) {
325
+ closedErr = 'expired';
326
+ return;
327
+ }
328
+ // 价格重判(中间可能有别人插队)
329
+ const curFresh = Number(fresh.current_price);
330
+ const isFirstFresh = Number(fresh.bid_count) === 0;
331
+ if (isFirstFresh) {
332
+ if (price < startingPrice) {
333
+ closedErr = `below_starting_${startingPrice}`;
334
+ return;
335
+ }
336
+ }
337
+ else {
338
+ const minNeed = Math.round((curFresh + Number(auc.min_increment)) * 100) / 100;
339
+ if (price < minNeed) {
340
+ closedErr = `below_min_${minNeed}`;
341
+ return;
342
+ }
343
+ }
344
+ // 释放本人之前的 active bid
345
+ const myPrev = db.prepare("SELECT id, stake_locked FROM auction_bids WHERE auction_id = ? AND buyer_id = ? AND status = 'active'").get(req.params.id, user.id);
346
+ if (myPrev) {
347
+ db.prepare("UPDATE auction_bids SET status = 'outbid', resolved_at = datetime('now') WHERE id = ?").run(myPrev.id);
348
+ if (myPrev.stake_locked > 0)
349
+ db.prepare('UPDATE wallets SET balance = balance + ?, staked = staked - ? WHERE user_id = ?').run(myPrev.stake_locked, myPrev.stake_locked, user.id);
350
+ }
351
+ // 释放别人的最高 active bid
352
+ const others = db.prepare("SELECT id, buyer_id, stake_locked FROM auction_bids WHERE auction_id = ? AND status = 'active' AND buyer_id != ?").all(req.params.id, user.id);
353
+ for (const o of others) {
354
+ db.prepare("UPDATE auction_bids SET status = 'outbid', resolved_at = datetime('now') WHERE id = ?").run(o.id);
355
+ if (o.stake_locked > 0)
356
+ db.prepare('UPDATE wallets SET balance = balance + ?, staked = staked - ? WHERE user_id = ?').run(o.stake_locked, o.stake_locked, o.buyer_id);
357
+ }
358
+ // 插入新 bid
359
+ db.prepare(`INSERT INTO auction_bids (id, auction_id, buyer_id, price, stake_locked) VALUES (?,?,?,?,?)`)
360
+ .run(id, req.params.id, user.id, price, stake);
361
+ db.prepare('UPDATE wallets SET balance = balance - ?, staked = staked + ? WHERE user_id = ?').run(stake, stake, user.id);
362
+ // P1 #9:卖家 stake 动态补足(5% × current_price,余额不足则尽量补)
363
+ const targetSellerStake = Math.max(1, Math.round(price * AUC_SELLER_STAKE_PCT * 100) / 100);
364
+ const curSellerStake = Number(fresh.seller_stake_locked) || 0;
365
+ if (targetSellerStake > curSellerStake) {
366
+ const delta = Math.round((targetSellerStake - curSellerStake) * 100) / 100;
367
+ const sWal = db.prepare('SELECT balance FROM wallets WHERE user_id = ?').get(auc.seller_id);
368
+ const canTopup = sWal ? Math.min(delta, Number(sWal.balance)) : 0;
369
+ if (canTopup > 0) {
370
+ db.prepare('UPDATE wallets SET balance = balance - ?, staked = staked + ? WHERE user_id = ?').run(canTopup, canTopup, auc.seller_id);
371
+ db.prepare('UPDATE auctions SET seller_stake_locked = seller_stake_locked + ? WHERE id = ?').run(canTopup, req.params.id);
372
+ sellerTopup = canTopup;
373
+ }
374
+ }
375
+ // P1 #5:反狙击延长(max_extends 上限保护)
376
+ const sniperMin = Number(fresh.sniper_extend_min || 0);
377
+ const deadlineMs = Date.parse(String(fresh.deadline_at).replace(' ', 'T') + 'Z');
378
+ const nowMs = Date.now();
379
+ const inSnipeWindow = sniperMin > 0 && deadlineMs - nowMs < sniperMin * 60_000;
380
+ const canExtend = Number(fresh.extends_used) < Number(fresh.max_extends || 10);
381
+ if (inSnipeWindow && canExtend) {
382
+ newDeadlineExt = sniperMin;
383
+ db.prepare(`UPDATE auctions SET current_price = ?, bid_count = bid_count + 1,
384
+ deadline_at = datetime(deadline_at, '+' || ? || ' minutes'),
385
+ extends_used = extends_used + 1,
386
+ updated_at = datetime('now') WHERE id = ?`).run(price, sniperMin, req.params.id);
387
+ }
388
+ else {
389
+ db.prepare(`UPDATE auctions SET current_price = ?, bid_count = bid_count + 1, updated_at = datetime('now') WHERE id = ?`).run(price, req.params.id);
390
+ }
391
+ })();
392
+ if (closedErr) {
393
+ const ce = closedErr;
394
+ return void res.json({ error: ce === 'expired' ? '拍卖已到期' : ce === 'not_found' ? '拍卖不存在' : ce.startsWith('below_') ? '出价不足,请刷新页面查看当前最高价' : `拍卖已结束(${ce})` });
395
+ }
396
+ // 通知卖家 + 被超越的买家
397
+ try {
398
+ db.prepare(`INSERT INTO notifications (id, user_id, type, title, body, created_at)
399
+ VALUES (?,?,'auction_new_bid',?,?,datetime('now'))`)
400
+ .run(generateId('ntf'), auc.seller_id, `🔨 新出价 ${price} WAZ`, `拍卖:${String(auc.title).slice(0, 30)}`);
401
+ }
402
+ catch { }
403
+ res.json({ id, stake_locked: stake, current_price: price, sniper_extended_min: newDeadlineExt, seller_topup: sellerTopup || undefined });
404
+ });
405
+ // 卖家:取消(仅未出价时)
406
+ app.delete('/api/auctions/:id', (req, res) => {
407
+ const user = auth(req, res);
408
+ if (!user)
409
+ return;
410
+ const auc = db.prepare('SELECT * FROM auctions WHERE id = ?').get(req.params.id);
411
+ if (!auc)
412
+ return void res.status(404).json({ error: '拍卖不存在' });
413
+ if (auc.seller_id !== user.id)
414
+ return void res.status(403).json({ error: '仅卖家本人可取消' });
415
+ if (auc.status !== 'open')
416
+ return void res.json({ error: `当前状态 ${auc.status} 不可取消` });
417
+ if (Number(auc.bid_count) > 0)
418
+ return void res.json({ error: '已有买家出价,无法取消' });
419
+ const sellerStake = Number(auc.seller_stake_locked) || 0;
420
+ db.transaction(() => {
421
+ db.prepare("UPDATE auctions SET status = 'cancelled', updated_at = datetime('now') WHERE id = ?").run(req.params.id);
422
+ if (sellerStake > 0)
423
+ db.prepare('UPDATE wallets SET balance = balance + ?, staked = staked - ? WHERE user_id = ?').run(sellerStake, sellerStake, user.id);
424
+ if (auc.product_id)
425
+ db.prepare("UPDATE products SET status = 'active', updated_at = datetime('now') WHERE id = ? AND status = 'auction_pending'").run(auc.product_id);
426
+ })();
427
+ res.json({ success: true, stake_released: sellerStake });
428
+ });
429
+ // Admin 手动跑提醒派发
430
+ app.post('/api/admin/auction-reminders/run', (req, res) => {
431
+ const admin = requireProtocolAdmin(req, res);
432
+ if (!admin)
433
+ return;
434
+ res.json(fireDueAuctionReminders(db, generateId));
435
+ });
436
+ }
@@ -0,0 +1,40 @@
1
+ export function registerAuthLoginRoutes(app, deps) {
2
+ const { db, INTERNAL_AUDITOR_ID, isLocked, verifyPassword, recordFailure, resetFailures, recordSession } = deps;
3
+ app.post('/api/login', (req, res) => {
4
+ const { name, password } = req.body;
5
+ if (!name?.trim() || !password)
6
+ return void res.json({ error: '请填写用户名 / 昵称和密码' });
7
+ const ref = name.trim().replace(/^@/, '').toLowerCase();
8
+ let matches = db.prepare("SELECT * FROM users WHERE handle = ? AND id NOT IN ('sys_protocol', ?)").all(ref, INTERNAL_AUDITOR_ID);
9
+ if (matches.length === 0) {
10
+ matches = db.prepare("SELECT * FROM users WHERE name = ? AND id NOT IN ('sys_protocol', ?)").all(name.trim(), INTERNAL_AUDITOR_ID);
11
+ }
12
+ if (matches.length === 0)
13
+ return void res.json({ error: '账号或密码错误' });
14
+ if (matches.length > 1)
15
+ return void res.json({ error: '该昵称对应多个账户,请改用 @用户名 或 API Key 登录' });
16
+ const user = matches[0];
17
+ if (isLocked(user)) {
18
+ const minutes = Math.ceil((new Date(user.locked_until).getTime() - Date.now()) / 60_000);
19
+ return void res.json({ error: `账户已临时锁定,约 ${minutes} 分钟后再试` });
20
+ }
21
+ if (!user.password_hash)
22
+ return void res.json({ error: '该账户未设置密码,请使用 API Key 登录' });
23
+ if (!verifyPassword(String(password), user.password_hash)) {
24
+ recordFailure(user.id, user.failed_attempts || 0);
25
+ return void res.json({ error: '名称或密码错误' });
26
+ }
27
+ resetFailures(user.id);
28
+ try {
29
+ recordSession(user.id, user.api_key, req);
30
+ }
31
+ catch { }
32
+ res.json({
33
+ success: true,
34
+ api_key: user.api_key,
35
+ user_id: user.id,
36
+ name: user.name,
37
+ role: user.role,
38
+ });
39
+ });
40
+ }
@@ -0,0 +1,66 @@
1
+ export function registerAuthReadRoutes(app, deps) {
2
+ const { db, auth, safeRoles, getRegionMaxLevels, userMlmGate, getUserLevel } = deps;
3
+ app.get('/api/me', (req, res) => {
4
+ const user = auth(req, res);
5
+ if (!user)
6
+ return;
7
+ const wallet = db.prepare('SELECT * FROM wallets WHERE user_id = ?').get(user.id);
8
+ let roles = [];
9
+ try {
10
+ roles = JSON.parse(user.roles || JSON.stringify([user.role]));
11
+ }
12
+ catch {
13
+ roles = [user.role];
14
+ }
15
+ const region = user.region || 'global';
16
+ const maxLevels = getRegionMaxLevels(region);
17
+ res.json({ ...user, api_key: undefined, roles, wallet: wallet || null, region_max_levels: maxLevels });
18
+ });
19
+ app.get('/api/profile', (req, res) => {
20
+ const user = auth(req, res);
21
+ if (!user)
22
+ return;
23
+ const wallet = db.prepare('SELECT balance, staked, escrowed, earned FROM wallets WHERE user_id = ?').get(user.id);
24
+ const roles = safeRoles(user);
25
+ const pv = db.prepare("SELECT total_left_pv, total_right_pv FROM users WHERE id = ?").get(user.id);
26
+ const pendingScore = db.prepare("SELECT COALESCE(SUM(score),0) as s FROM binary_score_records WHERE user_id = ? AND settled_at IS NULL").get(user.id).s;
27
+ res.json({
28
+ id: user.id, name: user.name, role: user.role, roles, api_key: user.api_key, wallet: wallet || null,
29
+ permanent_code: user.permanent_code ?? null,
30
+ handle: user.handle ?? null,
31
+ handle_last_created_at: user.handle_last_created_at ?? null,
32
+ handle_change_log: (() => { try {
33
+ return JSON.parse(user.handle_change_log || '[]');
34
+ }
35
+ catch {
36
+ return [];
37
+ } })(),
38
+ email: user.email ?? null,
39
+ email_verified: !!user.email_verified,
40
+ phone: user.phone ?? null,
41
+ phone_verified: !!user.phone_verified,
42
+ has_password: !!user.password_hash,
43
+ region: user.region ?? 'global',
44
+ region_max_levels: getRegionMaxLevels(user.region || 'global'),
45
+ ...(() => { const g = userMlmGate(user.region || 'global'); return { mlm_ui_visible: g.mlmUiVisible, mlm_payout_levels: g.payoutLevels }; })(),
46
+ bio: user.bio ?? null,
47
+ search_anchor: user.search_anchor ?? null,
48
+ feed_visible: user.feed_visible == null ? 1 : Number(user.feed_visible),
49
+ default_address_text: user.default_address_text ?? null,
50
+ default_address_region: user.default_address_region ?? null,
51
+ default_address: (() => {
52
+ try {
53
+ return user.default_address_json ? JSON.parse(user.default_address_json) : null;
54
+ }
55
+ catch {
56
+ return null;
57
+ }
58
+ })(),
59
+ pending_score: Number(pendingScore),
60
+ total_left_pv: Number(pv?.total_left_pv ?? 0),
61
+ total_right_pv: Number(pv?.total_right_pv ?? 0),
62
+ lifetime_score: Number(user.lifetime_score ?? 0),
63
+ user_level: getUserLevel(Number(user.lifetime_score ?? 0)),
64
+ });
65
+ });
66
+ }