@seasonkoh/webaz 0.1.8 → 0.1.10
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/LICENSE +48 -0
- package/README.md +156 -20
- package/dist/layer0-foundation/L0-1-database/schema.js +5 -4
- package/dist/layer0-foundation/L0-2-state-machine/engine.js +228 -7
- package/dist/layer0-foundation/L0-2-state-machine/order-chain.js +156 -0
- package/dist/layer0-foundation/L0-2-state-machine/transitions.js +53 -12
- package/dist/layer0-foundation/L0-5-manifest/manifest.js +14 -1
- package/dist/layer1-agent/L1-1-mcp-server/auth.js +1 -1
- package/dist/layer1-agent/L1-1-mcp-server/server.js +3679 -852
- package/dist/layer1-agent/L1-2-external-anchor/anchor-engine.js +324 -0
- package/dist/layer1-agent/L1-2-identity/agent-passport.js +100 -0
- package/dist/layer2-business/L2-6-notifications/notification-engine.js +72 -5
- package/dist/layer2-business/L2-7-snf/snf-engine.js +287 -0
- package/dist/layer2-business/L2-anchor-registry/anchor-registry.js +396 -0
- package/dist/layer2-business/L2-notes/note-photo-storage.js +133 -0
- package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +6 -6
- package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +246 -0
- package/dist/layer4-economics/L4-3-reputation/reputation-engine.js +95 -1
- package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +31 -2
- package/dist/layer4-economics/L4-4-skill-market/skill-listing-engine.js +358 -0
- package/dist/pwa/public/app.js +31362 -2320
- package/dist/pwa/public/i18n.js +5308 -111
- package/dist/pwa/public/icon.svg +11 -0
- package/dist/pwa/public/index.html +4 -1
- package/dist/pwa/public/manifest.json +39 -4
- package/dist/pwa/public/openapi.json +5946 -0
- package/dist/pwa/public/style.css +278 -5
- package/dist/pwa/public/sw.js +41 -2
- package/dist/pwa/public/vendor/jsQR.js +10102 -0
- package/dist/pwa/public/webaz-logo.png +0 -0
- package/dist/pwa/routes/account-deletion.js +53 -0
- package/dist/pwa/routes/addresses.js +105 -0
- package/dist/pwa/routes/admin-admins.js +151 -0
- package/dist/pwa/routes/admin-analytics.js +253 -0
- package/dist/pwa/routes/admin-atomic.js +21 -0
- package/dist/pwa/routes/admin-catalog.js +64 -0
- package/dist/pwa/routes/admin-editor-picks.js +45 -0
- package/dist/pwa/routes/admin-events.js +60 -0
- package/dist/pwa/routes/admin-health.js +66 -0
- package/dist/pwa/routes/admin-moderation.js +120 -0
- package/dist/pwa/routes/admin-ops.js +179 -0
- package/dist/pwa/routes/admin-protocol-params.js +79 -0
- package/dist/pwa/routes/admin-reports.js +154 -0
- package/dist/pwa/routes/admin-tokenomics.js +113 -0
- package/dist/pwa/routes/admin-users-lifecycle.js +237 -0
- package/dist/pwa/routes/admin-users-query.js +390 -0
- package/dist/pwa/routes/admin-verifier-flow.js +126 -0
- package/dist/pwa/routes/admin-verifier-whitelist.js +111 -0
- package/dist/pwa/routes/admin-wallet-ops.js +66 -0
- package/dist/pwa/routes/agent-buy.js +215 -0
- package/dist/pwa/routes/agent-governance.js +386 -0
- package/dist/pwa/routes/agent-reputation.js +34 -0
- package/dist/pwa/routes/ai.js +101 -0
- package/dist/pwa/routes/analytics.js +272 -0
- package/dist/pwa/routes/anchors.js +169 -0
- package/dist/pwa/routes/announcements.js +110 -0
- package/dist/pwa/routes/ap2-mandate.js +121 -0
- package/dist/pwa/routes/arbitrator.js +117 -0
- package/dist/pwa/routes/auction.js +436 -0
- package/dist/pwa/routes/auth-login.js +40 -0
- package/dist/pwa/routes/auth-read.js +66 -0
- package/dist/pwa/routes/auth-register.js +161 -0
- package/dist/pwa/routes/auth-sessions.js +62 -0
- package/dist/pwa/routes/blocklist.js +60 -0
- package/dist/pwa/routes/buyer-feeds.js +224 -0
- package/dist/pwa/routes/cart.js +155 -0
- package/dist/pwa/routes/charity.js +816 -0
- package/dist/pwa/routes/chat.js +318 -0
- package/dist/pwa/routes/checkin-tasks.js +122 -0
- package/dist/pwa/routes/checkout-helpers.js +106 -0
- package/dist/pwa/routes/claim-initiators.js +88 -0
- package/dist/pwa/routes/claim-verify.js +615 -0
- package/dist/pwa/routes/claim-voting.js +114 -0
- package/dist/pwa/routes/claim-withdrawals.js +20 -0
- package/dist/pwa/routes/coupons.js +165 -0
- package/dist/pwa/routes/dashboards.js +99 -0
- package/dist/pwa/routes/dispute-cases.js +267 -0
- package/dist/pwa/routes/disputes-read.js +358 -0
- package/dist/pwa/routes/disputes-write.js +475 -0
- package/dist/pwa/routes/evidence.js +86 -0
- package/dist/pwa/routes/external-anchors.js +107 -0
- package/dist/pwa/routes/feedback.js +270 -0
- package/dist/pwa/routes/flash-sales.js +130 -0
- package/dist/pwa/routes/follows.js +103 -0
- package/dist/pwa/routes/group-buys.js +208 -0
- package/dist/pwa/routes/growth.js +199 -0
- package/dist/pwa/routes/import-product.js +153 -0
- package/dist/pwa/routes/kyc.js +40 -0
- package/dist/pwa/routes/leaderboard.js +149 -0
- package/dist/pwa/routes/listings.js +281 -0
- package/dist/pwa/routes/logistics.js +35 -0
- package/dist/pwa/routes/manifests.js +126 -0
- package/dist/pwa/routes/me-data.js +101 -0
- package/dist/pwa/routes/notifications.js +48 -0
- package/dist/pwa/routes/offers.js +96 -0
- package/dist/pwa/routes/orders-action.js +285 -0
- package/dist/pwa/routes/orders-create.js +382 -0
- package/dist/pwa/routes/orders-read.js +180 -0
- package/dist/pwa/routes/p2p-products.js +178 -0
- package/dist/pwa/routes/payments-governance.js +311 -0
- package/dist/pwa/routes/peers.js +34 -0
- package/dist/pwa/routes/pin-receipts.js +39 -0
- package/dist/pwa/routes/products-aliases.js +119 -0
- package/dist/pwa/routes/products-claims.js +60 -0
- package/dist/pwa/routes/products-create.js +206 -0
- package/dist/pwa/routes/products-crud.js +73 -0
- package/dist/pwa/routes/products-links.js +129 -0
- package/dist/pwa/routes/products-list.js +424 -0
- package/dist/pwa/routes/products-meta.js +155 -0
- package/dist/pwa/routes/products-update.js +125 -0
- package/dist/pwa/routes/profile-credentials.js +105 -0
- package/dist/pwa/routes/profile-identity.js +174 -0
- package/dist/pwa/routes/profile-location.js +35 -0
- package/dist/pwa/routes/profile-placement.js +70 -0
- package/dist/pwa/routes/profile-prefs.js +93 -0
- package/dist/pwa/routes/promoter.js +208 -0
- package/dist/pwa/routes/public-utils.js +227 -0
- package/dist/pwa/routes/push.js +54 -0
- package/dist/pwa/routes/ratings.js +220 -0
- package/dist/pwa/routes/recover-key.js +100 -0
- package/dist/pwa/routes/referral.js +58 -0
- package/dist/pwa/routes/reputation.js +34 -0
- package/dist/pwa/routes/returns.js +493 -0
- package/dist/pwa/routes/reviews.js +81 -0
- package/dist/pwa/routes/rfqs.js +443 -0
- package/dist/pwa/routes/search.js +172 -0
- package/dist/pwa/routes/secondhand.js +278 -0
- package/dist/pwa/routes/seller-quota.js +225 -0
- package/dist/pwa/routes/share-redirects.js +164 -0
- package/dist/pwa/routes/shareables-interactions.js +212 -0
- package/dist/pwa/routes/shareables.js +470 -0
- package/dist/pwa/routes/shops.js +98 -0
- package/dist/pwa/routes/signaling.js +43 -0
- package/dist/pwa/routes/skill-market.js +173 -0
- package/dist/pwa/routes/skills.js +174 -0
- package/dist/pwa/routes/snf.js +126 -0
- package/dist/pwa/routes/tags.js +47 -0
- package/dist/pwa/routes/trial.js +333 -0
- package/dist/pwa/routes/trusted-kpi.js +87 -0
- package/dist/pwa/routes/url-claim.js +113 -0
- package/dist/pwa/routes/users-public.js +317 -0
- package/dist/pwa/routes/variants.js +156 -0
- package/dist/pwa/routes/verifier-user.js +107 -0
- package/dist/pwa/routes/verify-tasks.js +120 -0
- package/dist/pwa/routes/waitlist.js +65 -0
- package/dist/pwa/routes/wallet-read.js +218 -0
- package/dist/pwa/routes/wallet-write.js +273 -0
- package/dist/pwa/routes/webauthn.js +188 -0
- package/dist/pwa/routes/webhooks.js +162 -0
- package/dist/pwa/routes/welcome.js +226 -0
- package/dist/pwa/routes/wishlist-qa.js +135 -0
- package/dist/pwa/security/ssrf.js +110 -0
- package/dist/pwa/server.js +9317 -2097
- package/package.json +8 -3
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { recordRatingReputation } from '../../layer4-economics/L4-3-reputation/reputation-engine.js';
|
|
2
|
+
const RATING_BLIND_DAYS = 14;
|
|
3
|
+
function parseDim(v) {
|
|
4
|
+
const n = Number(v);
|
|
5
|
+
return Number.isInteger(n) && n >= 1 && n <= 5 ? n : null;
|
|
6
|
+
}
|
|
7
|
+
export function registerRatingsRoutes(app, deps) {
|
|
8
|
+
const { db, generateId, auth, isTrustedRole, errorRes, broadcastSystemEvent } = deps;
|
|
9
|
+
// buyer → seller 评价(一单一评,仅 completed 订单可评)
|
|
10
|
+
app.post('/api/orders/:order_id/rating', (req, res) => {
|
|
11
|
+
const user = auth(req, res);
|
|
12
|
+
if (!user)
|
|
13
|
+
return;
|
|
14
|
+
if (isTrustedRole(user))
|
|
15
|
+
return void errorRes(res, 403, 'TRUSTED_ROLE_NO_TRADE', '受信角色无购物功能');
|
|
16
|
+
const order = db.prepare('SELECT id, buyer_id, seller_id, product_id, status FROM orders WHERE id = ?').get(req.params.order_id);
|
|
17
|
+
if (!order)
|
|
18
|
+
return void res.status(404).json({ error: '订单不存在' });
|
|
19
|
+
if (order.buyer_id !== user.id)
|
|
20
|
+
return void res.status(403).json({ error: '仅买家可评价' });
|
|
21
|
+
if (order.status !== 'completed')
|
|
22
|
+
return void res.status(400).json({ error: '订单完成后才能评价' });
|
|
23
|
+
const existing = db.prepare('SELECT order_id FROM order_ratings WHERE order_id = ?').get(order.id);
|
|
24
|
+
if (existing)
|
|
25
|
+
return void res.status(400).json({ error: '已评价过,每单仅可评一次' });
|
|
26
|
+
const stars = Number(req.body?.stars);
|
|
27
|
+
if (!Number.isInteger(stars) || stars < 1 || stars > 5)
|
|
28
|
+
return void res.status(400).json({ error: '评分需在 1-5 之间' });
|
|
29
|
+
const comment = req.body?.comment ? String(req.body.comment).slice(0, 1000) : null;
|
|
30
|
+
const dimQuality = parseDim(req.body?.dim_quality);
|
|
31
|
+
const dimSpeed = parseDim(req.body?.dim_speed);
|
|
32
|
+
const dimService = parseDim(req.body?.dim_service);
|
|
33
|
+
const hiddenUntil = new Date(Date.now() + RATING_BLIND_DAYS * 24 * 3600 * 1000).toISOString();
|
|
34
|
+
db.transaction(() => {
|
|
35
|
+
db.prepare(`INSERT INTO order_ratings (order_id, buyer_id, seller_id, product_id, stars, comment, dim_quality, dim_speed, dim_service, hidden_until)
|
|
36
|
+
VALUES (?,?,?,?,?,?,?,?,?,?)`)
|
|
37
|
+
.run(order.id, order.buyer_id, order.seller_id, order.product_id, stars, comment, dimQuality, dimSpeed, dimService, hiddenUntil);
|
|
38
|
+
try {
|
|
39
|
+
db.prepare(`INSERT INTO notifications (id, user_id, title, body, order_id) VALUES (?,?,?,?,?)`)
|
|
40
|
+
.run(generateId('ntf'), order.seller_id, `⭐ 收到 ${stars} 星评价`, comment ? String(comment).slice(0, 100) : '买家未留言', order.id);
|
|
41
|
+
}
|
|
42
|
+
catch { }
|
|
43
|
+
// L2-5 反哺声誉
|
|
44
|
+
try {
|
|
45
|
+
recordRatingReputation(db, { orderId: order.id, revieweeId: order.seller_id, revieweeRole: 'seller', stars });
|
|
46
|
+
}
|
|
47
|
+
catch (e) {
|
|
48
|
+
console.warn('[rating→rep] seller delta failed:', e.message);
|
|
49
|
+
}
|
|
50
|
+
})();
|
|
51
|
+
try {
|
|
52
|
+
broadcastSystemEvent('rating', '⭐', `${stars} 星评价 (订单 ${order.id})`, order.id);
|
|
53
|
+
}
|
|
54
|
+
catch { }
|
|
55
|
+
res.json({ success: true });
|
|
56
|
+
});
|
|
57
|
+
// seller → buyer 反向评价
|
|
58
|
+
app.post('/api/orders/:order_id/buyer-rating', (req, res) => {
|
|
59
|
+
const user = auth(req, res);
|
|
60
|
+
if (!user)
|
|
61
|
+
return;
|
|
62
|
+
const order = db.prepare('SELECT id, buyer_id, seller_id, status FROM orders WHERE id = ?').get(req.params.order_id);
|
|
63
|
+
if (!order)
|
|
64
|
+
return void res.status(404).json({ error: '订单不存在' });
|
|
65
|
+
if (order.seller_id !== user.id)
|
|
66
|
+
return void res.status(403).json({ error: '仅卖家可评价买家' });
|
|
67
|
+
if (order.status !== 'completed')
|
|
68
|
+
return void res.status(400).json({ error: '订单完成后才能评价' });
|
|
69
|
+
const existing = db.prepare('SELECT order_id FROM buyer_ratings WHERE order_id = ?').get(order.id);
|
|
70
|
+
if (existing)
|
|
71
|
+
return void res.status(400).json({ error: '已评价过,每单仅可评一次' });
|
|
72
|
+
const stars = Number(req.body?.stars);
|
|
73
|
+
if (!Number.isInteger(stars) || stars < 1 || stars > 5)
|
|
74
|
+
return void res.status(400).json({ error: '评分需在 1-5 之间' });
|
|
75
|
+
const comment = req.body?.comment ? String(req.body.comment).slice(0, 1000) : null;
|
|
76
|
+
const dimPay = parseDim(req.body?.dim_payment_speed);
|
|
77
|
+
const dimCom = parseDim(req.body?.dim_communication);
|
|
78
|
+
const dimRsp = parseDim(req.body?.dim_responsiveness);
|
|
79
|
+
const hiddenUntil = new Date(Date.now() + RATING_BLIND_DAYS * 24 * 3600 * 1000).toISOString();
|
|
80
|
+
db.transaction(() => {
|
|
81
|
+
db.prepare(`INSERT INTO buyer_ratings (order_id, seller_id, buyer_id, stars, comment, dim_payment_speed, dim_communication, dim_responsiveness, hidden_until)
|
|
82
|
+
VALUES (?,?,?,?,?,?,?,?,?)`)
|
|
83
|
+
.run(order.id, order.seller_id, order.buyer_id, stars, comment, dimPay, dimCom, dimRsp, hiddenUntil);
|
|
84
|
+
try {
|
|
85
|
+
db.prepare(`INSERT INTO notifications (id, user_id, title, body, order_id) VALUES (?,?,?,?,?)`)
|
|
86
|
+
.run(generateId('ntf'), order.buyer_id, `⭐ 卖家给你 ${stars} 星评价`, comment ? String(comment).slice(0, 100) : '卖家未留言', order.id);
|
|
87
|
+
}
|
|
88
|
+
catch { }
|
|
89
|
+
try {
|
|
90
|
+
recordRatingReputation(db, { orderId: order.id, revieweeId: order.buyer_id, revieweeRole: 'buyer', stars });
|
|
91
|
+
}
|
|
92
|
+
catch (e) {
|
|
93
|
+
console.warn('[buyer-rating→rep] delta failed:', e.message);
|
|
94
|
+
}
|
|
95
|
+
})();
|
|
96
|
+
res.json({ success: true });
|
|
97
|
+
});
|
|
98
|
+
// 查 seller → buyer 评价(双盲遮蔽:buyer 看不到,除非自己也评过 OR 窗口到期)
|
|
99
|
+
app.get('/api/orders/:order_id/buyer-rating', (req, res) => {
|
|
100
|
+
const user = auth(req, res);
|
|
101
|
+
if (!user)
|
|
102
|
+
return;
|
|
103
|
+
const order = db.prepare('SELECT buyer_id, seller_id FROM orders WHERE id = ?').get(req.params.order_id);
|
|
104
|
+
if (!order)
|
|
105
|
+
return void res.status(404).json({ error: '订单不存在' });
|
|
106
|
+
if (order.buyer_id !== user.id && order.seller_id !== user.id) {
|
|
107
|
+
return void res.status(403).json({ error: '无权查看' });
|
|
108
|
+
}
|
|
109
|
+
const br = db.prepare(`SELECT stars, comment, dim_payment_speed, dim_communication, dim_responsiveness, hidden_until, created_at FROM buyer_ratings WHERE order_id = ?`).get(req.params.order_id);
|
|
110
|
+
if (!br)
|
|
111
|
+
return void res.json({ item: null });
|
|
112
|
+
const isBuyerView = order.buyer_id === user.id;
|
|
113
|
+
const buyerAlsoRated = !!db.prepare(`SELECT order_id FROM order_ratings WHERE order_id = ?`).get(req.params.order_id);
|
|
114
|
+
const blindExpired = br.hidden_until && new Date(br.hidden_until) < new Date();
|
|
115
|
+
if (isBuyerView && !buyerAlsoRated && !blindExpired) {
|
|
116
|
+
return void res.json({ item: { masked: true, hidden_until: br.hidden_until, reason: 'blind_until_both_or_expire' } });
|
|
117
|
+
}
|
|
118
|
+
res.json({ item: br });
|
|
119
|
+
});
|
|
120
|
+
// 查 buyer → seller 评价(双盲遮蔽:seller 视角同样)
|
|
121
|
+
app.get('/api/orders/:order_id/rating', (req, res) => {
|
|
122
|
+
const user = auth(req, res);
|
|
123
|
+
if (!user)
|
|
124
|
+
return;
|
|
125
|
+
const order = db.prepare('SELECT buyer_id, seller_id FROM orders WHERE id = ?').get(req.params.order_id);
|
|
126
|
+
if (!order)
|
|
127
|
+
return void res.status(404).json({ error: '订单不存在' });
|
|
128
|
+
if (order.buyer_id !== user.id && order.seller_id !== user.id) {
|
|
129
|
+
return void res.status(403).json({ error: '无权查看' });
|
|
130
|
+
}
|
|
131
|
+
const r = db.prepare('SELECT stars, comment, reply, replied_at, buyer_followup, buyer_followup_at, dim_quality, dim_speed, dim_service, hidden_until, created_at FROM order_ratings WHERE order_id = ?').get(req.params.order_id);
|
|
132
|
+
if (!r)
|
|
133
|
+
return void res.json({ item: null });
|
|
134
|
+
const isSellerView = order.seller_id === user.id;
|
|
135
|
+
const sellerAlsoRated = !!db.prepare(`SELECT order_id FROM buyer_ratings WHERE order_id = ?`).get(req.params.order_id);
|
|
136
|
+
const blindExpired = r.hidden_until && new Date(r.hidden_until) < new Date();
|
|
137
|
+
if (isSellerView && !sellerAlsoRated && !blindExpired) {
|
|
138
|
+
return void res.json({ item: { masked: true, hidden_until: r.hidden_until, reason: 'blind_until_both_or_expire' } });
|
|
139
|
+
}
|
|
140
|
+
res.json({ item: r });
|
|
141
|
+
});
|
|
142
|
+
app.post('/api/orders/:order_id/rating/reply', (req, res) => {
|
|
143
|
+
const user = auth(req, res);
|
|
144
|
+
if (!user)
|
|
145
|
+
return;
|
|
146
|
+
const r = db.prepare('SELECT seller_id, reply FROM order_ratings WHERE order_id = ?').get(req.params.order_id);
|
|
147
|
+
if (!r)
|
|
148
|
+
return void res.status(404).json({ error: '该订单暂无评价' });
|
|
149
|
+
if (r.seller_id !== user.id)
|
|
150
|
+
return void res.status(403).json({ error: '仅卖家可回复' });
|
|
151
|
+
if (r.reply)
|
|
152
|
+
return void res.status(400).json({ error: '已回复过,每条评价仅可回复一次' });
|
|
153
|
+
const reply = req.body?.reply ? String(req.body.reply).slice(0, 500) : null;
|
|
154
|
+
if (!reply)
|
|
155
|
+
return void res.status(400).json({ error: '回复不能为空' });
|
|
156
|
+
db.prepare(`UPDATE order_ratings SET reply = ?, replied_at = datetime('now') WHERE order_id = ?`).run(reply, req.params.order_id);
|
|
157
|
+
res.json({ success: true });
|
|
158
|
+
});
|
|
159
|
+
// W3 买家追问 — 在卖家 reply 后可追问一次
|
|
160
|
+
app.post('/api/orders/:order_id/rating/followup', (req, res) => {
|
|
161
|
+
const user = auth(req, res);
|
|
162
|
+
if (!user)
|
|
163
|
+
return;
|
|
164
|
+
const r = db.prepare('SELECT buyer_id, reply, buyer_followup FROM order_ratings WHERE order_id = ?').get(req.params.order_id);
|
|
165
|
+
if (!r)
|
|
166
|
+
return void res.status(404).json({ error: '该订单暂无评价' });
|
|
167
|
+
if (r.buyer_id !== user.id)
|
|
168
|
+
return void res.status(403).json({ error: '仅买家可追问' });
|
|
169
|
+
if (!r.reply)
|
|
170
|
+
return void res.status(400).json({ error: '卖家尚未回复,无法追问' });
|
|
171
|
+
if (r.buyer_followup)
|
|
172
|
+
return void res.status(400).json({ error: '已追问过一次(每条评价最多一次追问)' });
|
|
173
|
+
const followup = req.body?.followup ? String(req.body.followup).trim().slice(0, 200) : '';
|
|
174
|
+
if (followup.length < 2)
|
|
175
|
+
return void res.status(400).json({ error: '追问内容至少 2 字' });
|
|
176
|
+
db.prepare(`UPDATE order_ratings SET buyer_followup = ?, buyer_followup_at = datetime('now') WHERE order_id = ?`)
|
|
177
|
+
.run(followup, req.params.order_id);
|
|
178
|
+
res.json({ success: true });
|
|
179
|
+
});
|
|
180
|
+
// 公开:商品评价 + 聚合(仅展示双盲已揭晓的)
|
|
181
|
+
app.get('/api/products/:product_id/ratings', (req, res) => {
|
|
182
|
+
const limit = Math.min(50, Math.max(1, Number(req.query.limit) || 20));
|
|
183
|
+
const blindOpen = `(EXISTS (SELECT 1 FROM buyer_ratings br WHERE br.order_id = r.order_id) OR r.hidden_until IS NULL OR datetime(r.hidden_until) <= datetime('now'))`;
|
|
184
|
+
const rows = db.prepare(`
|
|
185
|
+
SELECT r.stars, r.comment, r.reply, r.replied_at, r.buyer_followup, r.buyer_followup_at, r.created_at,
|
|
186
|
+
r.dim_quality, r.dim_speed, r.dim_service,
|
|
187
|
+
u.name as buyer_name, u.handle as buyer_handle
|
|
188
|
+
FROM order_ratings r
|
|
189
|
+
JOIN users u ON u.id = r.buyer_id
|
|
190
|
+
WHERE r.product_id = ? AND ${blindOpen}
|
|
191
|
+
ORDER BY r.created_at DESC LIMIT ?
|
|
192
|
+
`).all(req.params.product_id, limit);
|
|
193
|
+
const agg = db.prepare(`
|
|
194
|
+
SELECT COUNT(*) as cnt, COALESCE(AVG(stars), 0) as avg_stars,
|
|
195
|
+
SUM(CASE WHEN stars = 5 THEN 1 ELSE 0 END) as s5,
|
|
196
|
+
SUM(CASE WHEN stars = 4 THEN 1 ELSE 0 END) as s4,
|
|
197
|
+
SUM(CASE WHEN stars = 3 THEN 1 ELSE 0 END) as s3,
|
|
198
|
+
SUM(CASE WHEN stars = 2 THEN 1 ELSE 0 END) as s2,
|
|
199
|
+
SUM(CASE WHEN stars = 1 THEN 1 ELSE 0 END) as s1
|
|
200
|
+
FROM order_ratings r WHERE product_id = ? AND ${blindOpen}
|
|
201
|
+
`).get(req.params.product_id);
|
|
202
|
+
res.json({ items: rows, agg });
|
|
203
|
+
});
|
|
204
|
+
// 公开:卖家评价聚合(卖家主页)
|
|
205
|
+
app.get('/api/sellers/:seller_id/ratings', (req, res) => {
|
|
206
|
+
const limit = Math.min(50, Math.max(1, Number(req.query.limit) || 20));
|
|
207
|
+
const rows = db.prepare(`
|
|
208
|
+
SELECT r.stars, r.comment, r.reply, r.replied_at, r.buyer_followup, r.buyer_followup_at, r.created_at, r.product_id,
|
|
209
|
+
p.title as product_title,
|
|
210
|
+
u.name as buyer_name, u.handle as buyer_handle
|
|
211
|
+
FROM order_ratings r
|
|
212
|
+
JOIN products p ON p.id = r.product_id
|
|
213
|
+
JOIN users u ON u.id = r.buyer_id
|
|
214
|
+
WHERE r.seller_id = ?
|
|
215
|
+
ORDER BY r.created_at DESC LIMIT ?
|
|
216
|
+
`).all(req.params.seller_id, limit);
|
|
217
|
+
const agg = db.prepare(`SELECT COUNT(*) as cnt, COALESCE(AVG(stars), 0) as avg_stars FROM order_ratings WHERE seller_id = ?`).get(req.params.seller_id);
|
|
218
|
+
res.json({ items: rows, agg });
|
|
219
|
+
});
|
|
220
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
export function registerRecoverKeyRoutes(app, deps) {
|
|
2
|
+
const { db, internalAuditorId, issueCode, findActiveCode, CODE_TTL_MIN, MAX_CODE_ATTEMPTS } = deps;
|
|
3
|
+
// IP 级速率(5/min)— 防爆破列举账户
|
|
4
|
+
const recoverKeyHits = new Map();
|
|
5
|
+
app.post('/api/recover-key', (req, res) => {
|
|
6
|
+
const ip = req.ip || '';
|
|
7
|
+
if (ip) {
|
|
8
|
+
const now = Date.now();
|
|
9
|
+
const rec = recoverKeyHits.get(ip);
|
|
10
|
+
if (rec && now - rec.firstAt < 60_000) {
|
|
11
|
+
rec.count++;
|
|
12
|
+
if (rec.count > 5)
|
|
13
|
+
return void res.status(429).json({ error: '查询过于频繁,请 1 分钟后再试' });
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
recoverKeyHits.set(ip, { count: 1, firstAt: now });
|
|
17
|
+
}
|
|
18
|
+
if (recoverKeyHits.size > 1000) {
|
|
19
|
+
for (const [k, v] of recoverKeyHits)
|
|
20
|
+
if (now - v.firstAt > 60_000)
|
|
21
|
+
recoverKeyHits.delete(k);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const { name } = req.body;
|
|
25
|
+
if (!name?.trim())
|
|
26
|
+
return void res.json({ error: '请填写注册时使用的名称' });
|
|
27
|
+
const rows = db.prepare("SELECT name, role, api_key, email, phone, created_at FROM users WHERE name = ? AND id NOT IN ('sys_protocol', ?)").all(name.trim(), internalAuditorId);
|
|
28
|
+
if (rows.length === 0)
|
|
29
|
+
return void res.json({ error: '未找到该名称的账号' });
|
|
30
|
+
const mask = (s) => s && s.length > 8 ? `${s.slice(0, 4)}…${s.slice(-4)}` : s;
|
|
31
|
+
const maskEmail = (e) => {
|
|
32
|
+
if (!e)
|
|
33
|
+
return null;
|
|
34
|
+
const [u, d] = e.split('@');
|
|
35
|
+
if (!d)
|
|
36
|
+
return mask(e);
|
|
37
|
+
return `${u.slice(0, 1)}***@${d}`;
|
|
38
|
+
};
|
|
39
|
+
const maskPhone = (p) => p ? `${p.slice(0, 3)}****${p.slice(-4)}` : null;
|
|
40
|
+
const accounts = rows.map(r => ({
|
|
41
|
+
name: r.name,
|
|
42
|
+
role: r.role,
|
|
43
|
+
key_hint: mask(r.api_key), // 模糊辨认,不可登录
|
|
44
|
+
email_hint: maskEmail(r.email || null),
|
|
45
|
+
phone_hint: maskPhone(r.phone || null),
|
|
46
|
+
has_email: !!r.email,
|
|
47
|
+
has_phone: !!r.phone,
|
|
48
|
+
created_at: r.created_at,
|
|
49
|
+
}));
|
|
50
|
+
res.json({
|
|
51
|
+
found: accounts.length,
|
|
52
|
+
accounts,
|
|
53
|
+
notice: '完整密钥找回需通过已绑定的邮箱/手机验证(P1 即将上线)。若你此前未绑定任何渠道,请使用本机已保存的密钥登录或联系管理员。',
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
// 步骤 1:发送验证码到已绑定邮箱(防泄露:找没找到都同响应)
|
|
57
|
+
app.post('/api/recover-key/start', (req, res) => {
|
|
58
|
+
const { name, email } = req.body;
|
|
59
|
+
if (!name?.trim() || !email?.trim())
|
|
60
|
+
return void res.json({ error: '请填写名称和邮箱' });
|
|
61
|
+
const target = email.trim().toLowerCase();
|
|
62
|
+
const user = db.prepare(`
|
|
63
|
+
SELECT id, name, email FROM users
|
|
64
|
+
WHERE name = ? AND email = ? AND email_verified = 1
|
|
65
|
+
AND id NOT IN ('sys_protocol', ?) LIMIT 1
|
|
66
|
+
`).get(name.trim(), target, internalAuditorId);
|
|
67
|
+
if (user)
|
|
68
|
+
issueCode(user.id, 'email', target, 'recover_key');
|
|
69
|
+
res.json({
|
|
70
|
+
success: true,
|
|
71
|
+
notice: '若该名称与邮箱组合存在,验证码已发送至该邮箱',
|
|
72
|
+
expires_in_min: CODE_TTL_MIN,
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
// 步骤 2:提交验证码 → 返回完整 api_key
|
|
76
|
+
app.post('/api/recover-key/confirm', (req, res) => {
|
|
77
|
+
const { name, email, code } = req.body;
|
|
78
|
+
if (!name?.trim() || !email?.trim() || !code?.trim())
|
|
79
|
+
return void res.json({ error: '请填写完整信息' });
|
|
80
|
+
const target = email.trim().toLowerCase();
|
|
81
|
+
const row = findActiveCode('email', target, 'recover_key');
|
|
82
|
+
if (!row)
|
|
83
|
+
return void res.json({ error: '验证码已过期或未发送,请重新开始' });
|
|
84
|
+
const user = db.prepare(`SELECT id, name, api_key FROM users WHERE id = ?`).get(row.user_id);
|
|
85
|
+
if (!user || user.name !== name.trim())
|
|
86
|
+
return void res.json({ error: '名称与验证码不匹配' });
|
|
87
|
+
if (String(row.code) !== code.trim()) {
|
|
88
|
+
const attempts = row.attempts + 1;
|
|
89
|
+
if (attempts >= MAX_CODE_ATTEMPTS) {
|
|
90
|
+
db.prepare("UPDATE verification_codes SET attempts = ?, used_at = datetime('now') WHERE id = ?")
|
|
91
|
+
.run(attempts, row.id);
|
|
92
|
+
return void res.json({ error: '错误次数过多,验证码已作废,请重新开始' });
|
|
93
|
+
}
|
|
94
|
+
db.prepare("UPDATE verification_codes SET attempts = ? WHERE id = ?").run(attempts, row.id);
|
|
95
|
+
return void res.json({ error: `验证码错误(剩余 ${MAX_CODE_ATTEMPTS - attempts} 次)` });
|
|
96
|
+
}
|
|
97
|
+
db.prepare("UPDATE verification_codes SET used_at = datetime('now') WHERE id = ?").run(row.id);
|
|
98
|
+
res.json({ success: true, api_key: user.api_key, name: user.name });
|
|
99
|
+
});
|
|
100
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export function registerReferralRoutes(app, deps) {
|
|
2
|
+
const { db, auth, requireProtocolAdmin, logAdminAction, issueInviteSlot, inviteRotationLookup } = deps;
|
|
3
|
+
// B-1: 个人邀请 dashboard
|
|
4
|
+
app.get('/api/referral/me', (req, res) => {
|
|
5
|
+
const user = auth(req, res);
|
|
6
|
+
if (!user)
|
|
7
|
+
return;
|
|
8
|
+
const code = user.permanent_code || null;
|
|
9
|
+
// 我直接邀请的人
|
|
10
|
+
const directInvitees = db.prepare(`
|
|
11
|
+
SELECT u.id, u.handle, u.name, u.role, u.created_at,
|
|
12
|
+
(SELECT COUNT(*) FROM orders WHERE buyer_id = u.id AND status = 'completed') as completed_orders,
|
|
13
|
+
(SELECT COALESCE(SUM(total_amount), 0) FROM orders WHERE buyer_id = u.id AND status = 'completed') as gmv
|
|
14
|
+
FROM users u WHERE u.sponsor_id = ?
|
|
15
|
+
ORDER BY u.created_at DESC LIMIT 50
|
|
16
|
+
`).all(user.id);
|
|
17
|
+
// 推土机奖励 / 商品分享佣金(commission_records 按订单粒度)
|
|
18
|
+
const earnings = db.prepare(`
|
|
19
|
+
SELECT COUNT(*) as cnt, COALESCE(SUM(amount), 0) as total FROM commission_records WHERE beneficiary_id = ?
|
|
20
|
+
`).get(user.id);
|
|
21
|
+
const todayEarnings = db.prepare(`SELECT COALESCE(SUM(amount), 0) as t FROM commission_records WHERE beneficiary_id = ? AND created_at > datetime('now', '-1 day')`).get(user.id).t;
|
|
22
|
+
const monthEarnings = db.prepare(`SELECT COALESCE(SUM(amount), 0) as t FROM commission_records WHERE beneficiary_id = ? AND created_at > datetime('now', '-30 days')`).get(user.id).t;
|
|
23
|
+
res.json({
|
|
24
|
+
invite_code: code,
|
|
25
|
+
invite_link: code ? `${req.protocol}://${req.get('host')}/?ref=${code}` : null,
|
|
26
|
+
direct_invitees_count: directInvitees.length,
|
|
27
|
+
direct_invitees: directInvitees,
|
|
28
|
+
earnings: {
|
|
29
|
+
total_records: earnings.cnt,
|
|
30
|
+
total_waz: earnings.total,
|
|
31
|
+
today_waz: todayEarnings,
|
|
32
|
+
month_waz: monthEarnings,
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
// 公开邀请码轮询(开关 ON 时)
|
|
37
|
+
app.post('/api/invite/rotate', (_req, res) => {
|
|
38
|
+
const enabled = db.prepare("SELECT value FROM system_state WHERE key='invite_rotation_enabled'").get()?.value === '1';
|
|
39
|
+
if (!enabled)
|
|
40
|
+
return void res.status(403).json({ error: '邀请码获取暂未开放', enabled: false });
|
|
41
|
+
const slot = issueInviteSlot();
|
|
42
|
+
const u = inviteRotationLookup(slot);
|
|
43
|
+
if (!u)
|
|
44
|
+
return void res.status(503).json({ error: `轮询用户未就绪,请联系管理员`, enabled: true });
|
|
45
|
+
res.json({ enabled: true, code: u.code });
|
|
46
|
+
});
|
|
47
|
+
// protocol 开关
|
|
48
|
+
app.post('/api/admin/invite-rotation/toggle', (req, res) => {
|
|
49
|
+
const admin = requireProtocolAdmin(req, res);
|
|
50
|
+
if (!admin)
|
|
51
|
+
return;
|
|
52
|
+
const { enabled } = req.body;
|
|
53
|
+
const v = enabled ? '1' : '0';
|
|
54
|
+
db.prepare("INSERT OR REPLACE INTO system_state (key, value) VALUES ('invite_rotation_enabled', ?)").run(v);
|
|
55
|
+
logAdminAction(admin.id, 'invite_rotation_toggle', 'system', 'invite_rotation_enabled', { value: v });
|
|
56
|
+
res.json({ success: true, enabled: !!enabled });
|
|
57
|
+
});
|
|
58
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export function registerReputationRoutes(app, deps) {
|
|
2
|
+
const { db, auth, getReputation, getSellerMetrics } = deps;
|
|
3
|
+
app.get('/api/reputation', (req, res) => {
|
|
4
|
+
const user = auth(req, res);
|
|
5
|
+
if (!user)
|
|
6
|
+
return;
|
|
7
|
+
const rep = getReputation(db, user.id);
|
|
8
|
+
res.json({
|
|
9
|
+
level: rep.level,
|
|
10
|
+
total_points: rep.total_points,
|
|
11
|
+
transactions_done: rep.transactions_done,
|
|
12
|
+
disputes_won: rep.disputes_won,
|
|
13
|
+
disputes_lost: rep.disputes_lost,
|
|
14
|
+
violations: rep.violations,
|
|
15
|
+
recent_events: rep.recent_events,
|
|
16
|
+
metrics: getSellerMetrics(user.id),
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
app.get('/api/reputation/:userId', (req, res) => {
|
|
20
|
+
const rep = getReputation(db, req.params.userId);
|
|
21
|
+
const decayRow = db.prepare(`SELECT last_decay_at FROM reputation_scores WHERE user_id = ?`).get(req.params.userId);
|
|
22
|
+
res.json({
|
|
23
|
+
level: rep.level,
|
|
24
|
+
total_points: rep.total_points,
|
|
25
|
+
transactions_done: rep.transactions_done,
|
|
26
|
+
disputes_won: rep.disputes_won,
|
|
27
|
+
disputes_lost: rep.disputes_lost,
|
|
28
|
+
violations: rep.violations,
|
|
29
|
+
metrics: getSellerMetrics(req.params.userId),
|
|
30
|
+
last_decay_at: decayRow?.last_decay_at || null,
|
|
31
|
+
decay_rate: 0.02,
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|