@seasonkoh/webaz 0.1.24 → 0.1.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/layer0-foundation/L0-1-database/db-backends/pg-backend.js +51 -0
- package/dist/layer0-foundation/L0-1-database/db-backends/sql-dialect-datetime.js +437 -0
- package/dist/layer0-foundation/L0-1-database/db-backends/sql-placeholders.js +98 -0
- package/dist/layer0-foundation/L0-1-database/db.js +65 -0
- package/dist/layer0-foundation/L0-2-state-machine/order-chain.js +13 -11
- package/dist/layer0-foundation/L0-2-state-machine/transitions.js +1 -1
- package/dist/layer0-foundation/L0-5-manifest/manifest.js +13 -11
- package/dist/layer1-agent/L1-1-mcp-server/server.js +288 -208
- package/dist/layer1-agent/L1-2-external-anchor/anchor-engine.js +14 -12
- package/dist/layer2-business/L2-6-notifications/notification-engine.js +8 -5
- package/dist/layer2-business/L2-7-snf/snf-engine.js +16 -14
- package/dist/layer2-business/L2-8-feedback/build-feedback-engine.js +18 -10
- package/dist/layer2-business/L2-9-contribution/build-reputation-engine.js +37 -23
- package/dist/layer2-business/L2-9-contribution/build-task-agent-metadata-store.js +182 -0
- package/dist/layer2-business/L2-9-contribution/build-task-participation.js +47 -0
- package/dist/layer2-business/L2-9-contribution/build-task-read.js +222 -0
- package/dist/layer2-business/L2-9-contribution/build-tasks-engine.js +11 -3
- package/dist/layer2-business/L2-9-contribution/canonical-contribution-target.js +16 -0
- package/dist/layer2-business/L2-9-contribution/contribution-display-envelope.js +40 -0
- package/dist/layer2-business/L2-9-contribution/contribution-score-contract.js +36 -0
- package/dist/layer2-business/L2-9-contribution/contribution-score-evidence.js +61 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/canonical.js +60 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/github-credential.schema.js +140 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/github-fetch-adapter.js +437 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/self-consistency.js +38 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/verifier.js +231 -0
- package/dist/layer2-business/L2-9-contribution/github-credential-ingestion-engine.js +145 -0
- package/dist/layer2-business/L2-9-contribution/github-credential-store.js +115 -0
- package/dist/layer2-business/L2-9-contribution/identity-binding-engine.js +134 -0
- package/dist/layer2-business/L2-9-contribution/identity-binding-store.js +101 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-challenge-engine.js +126 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-challenge-store.js +30 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-discovery.js +55 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-engine.js +109 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-fact-precondition.js +22 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-proof-verifier.js +97 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-read.js +59 -0
- package/dist/layer2-business/L2-9-contribution/task-proposal-ai-store.js +99 -0
- package/dist/layer2-business/L2-9-contribution/task-proposal-draft.js +191 -0
- package/dist/layer2-business/L2-9-contribution/task-proposal-store.js +129 -0
- package/dist/layer2-business/L2-notes/note-photo-storage.js +4 -2
- package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +17 -15
- package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +11 -8
- package/dist/layer4-economics/L4-3-reputation/reputation-engine.js +9 -8
- package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +11 -8
- package/dist/layer4-economics/L4-4-skill-market/skill-listing-engine.js +22 -16
- package/dist/pwa/acp-feed.js +13 -1
- package/dist/pwa/admin-bearer-auth.js +21 -0
- package/dist/pwa/contract-fingerprint.js +2 -0
- package/dist/pwa/email-delivery.js +127 -0
- package/dist/pwa/endpoint-actions.js +5 -1
- package/dist/pwa/goal-index.js +8 -8
- package/dist/pwa/human-presence.js +62 -0
- package/dist/pwa/public/app.js +1485 -283
- package/dist/pwa/public/i18n.js +297 -59
- package/dist/pwa/public/index.html +1 -0
- package/dist/pwa/public/openapi.json +5 -5
- package/dist/pwa/public/whitepaper/en/index.html +153 -0
- package/dist/pwa/public/whitepaper/zh-CN/index.html +153 -0
- package/dist/pwa/rate-limit.js +22 -0
- package/dist/pwa/routes/account-deletion.js +15 -13
- package/dist/pwa/routes/addresses.js +10 -9
- package/dist/pwa/routes/admin-admins.js +13 -14
- package/dist/pwa/routes/admin-analytics.js +109 -69
- package/dist/pwa/routes/admin-atomic.js +10 -4
- package/dist/pwa/routes/admin-catalog.js +13 -11
- package/dist/pwa/routes/admin-editor-picks.js +15 -10
- package/dist/pwa/routes/admin-events.js +5 -3
- package/dist/pwa/routes/admin-health.js +2 -1
- package/dist/pwa/routes/admin-moderation.js +50 -29
- package/dist/pwa/routes/admin-ops.js +35 -23
- package/dist/pwa/routes/admin-protocol-params.js +16 -19
- package/dist/pwa/routes/admin-reports.js +23 -21
- package/dist/pwa/routes/admin-tokenomics.js +26 -25
- package/dist/pwa/routes/admin-users-lifecycle.js +37 -40
- package/dist/pwa/routes/admin-users-query.js +65 -53
- package/dist/pwa/routes/admin-verifier-flow.js +82 -41
- package/dist/pwa/routes/admin-verifier-whitelist.js +55 -27
- package/dist/pwa/routes/admin-wallet-ops.js +32 -7
- package/dist/pwa/routes/agent-buy.js +46 -22
- package/dist/pwa/routes/agent-governance.js +52 -56
- package/dist/pwa/routes/ai.js +7 -5
- package/dist/pwa/routes/analytics.js +43 -41
- package/dist/pwa/routes/anchors.js +19 -20
- package/dist/pwa/routes/announcements.js +13 -13
- package/dist/pwa/routes/arbitrator.js +97 -31
- package/dist/pwa/routes/auction.js +157 -116
- package/dist/pwa/routes/auth-login.js +6 -4
- package/dist/pwa/routes/auth-read.js +21 -10
- package/dist/pwa/routes/auth-register.js +111 -26
- package/dist/pwa/routes/auth-sessions.js +12 -11
- package/dist/pwa/routes/blocklist.js +16 -15
- package/dist/pwa/routes/build-feedback.js +10 -9
- package/dist/pwa/routes/build-reputation.js +6 -2
- package/dist/pwa/routes/build-tasks.js +45 -13
- package/dist/pwa/routes/buyer-feeds.js +27 -25
- package/dist/pwa/routes/cart.js +16 -15
- package/dist/pwa/routes/charity.js +212 -150
- package/dist/pwa/routes/chat.js +42 -43
- package/dist/pwa/routes/checkin-tasks.js +10 -9
- package/dist/pwa/routes/checkout-helpers.js +12 -10
- package/dist/pwa/routes/claim-initiators.js +34 -14
- package/dist/pwa/routes/claim-verify.js +86 -53
- package/dist/pwa/routes/claim-voting.js +43 -18
- package/dist/pwa/routes/contribution-identity.js +164 -0
- package/dist/pwa/routes/contribution-score.js +19 -0
- package/dist/pwa/routes/coupons.js +19 -16
- package/dist/pwa/routes/dashboards.js +18 -16
- package/dist/pwa/routes/dispute-cases.js +25 -24
- package/dist/pwa/routes/disputes-read.js +45 -51
- package/dist/pwa/routes/disputes-write.js +124 -61
- package/dist/pwa/routes/evidence.js +9 -9
- package/dist/pwa/routes/external-anchors.js +13 -12
- package/dist/pwa/routes/feedback.js +29 -33
- package/dist/pwa/routes/flash-sales.js +18 -16
- package/dist/pwa/routes/follows.js +25 -24
- package/dist/pwa/routes/governance-auto-deactivate.js +21 -9
- package/dist/pwa/routes/governance-onboarding.js +70 -59
- package/dist/pwa/routes/group-buys.js +22 -22
- package/dist/pwa/routes/growth.js +34 -31
- package/dist/pwa/routes/import-product.js +12 -10
- package/dist/pwa/routes/kyc.js +9 -8
- package/dist/pwa/routes/leaderboard.js +20 -18
- package/dist/pwa/routes/listings.js +23 -22
- package/dist/pwa/routes/logistics.js +10 -8
- package/dist/pwa/routes/manifests.js +27 -27
- package/dist/pwa/routes/me-data.js +23 -21
- package/dist/pwa/routes/notifications.js +7 -6
- package/dist/pwa/routes/offers.js +30 -12
- package/dist/pwa/routes/orders-action.js +51 -29
- package/dist/pwa/routes/orders-create.js +75 -20
- package/dist/pwa/routes/orders-read.js +21 -20
- package/dist/pwa/routes/p2p-products.js +30 -18
- package/dist/pwa/routes/payments-governance.js +61 -56
- package/dist/pwa/routes/peers.js +9 -8
- package/dist/pwa/routes/pin-receipts.js +13 -13
- package/dist/pwa/routes/products-aliases.js +12 -10
- package/dist/pwa/routes/products-claims.js +36 -17
- package/dist/pwa/routes/products-create.js +53 -38
- package/dist/pwa/routes/products-crud.js +17 -16
- package/dist/pwa/routes/products-links.js +49 -26
- package/dist/pwa/routes/products-list.js +6 -4
- package/dist/pwa/routes/products-meta.js +40 -39
- package/dist/pwa/routes/products-update.js +19 -5
- package/dist/pwa/routes/profile-credentials.js +20 -19
- package/dist/pwa/routes/profile-identity.js +14 -13
- package/dist/pwa/routes/profile-location.js +7 -6
- package/dist/pwa/routes/profile-placement.js +20 -19
- package/dist/pwa/routes/profile-prefs.js +11 -11
- package/dist/pwa/routes/promoter.js +58 -66
- package/dist/pwa/routes/public-build-tasks.js +19 -0
- package/dist/pwa/routes/public-utils.js +108 -46
- package/dist/pwa/routes/push.js +16 -15
- package/dist/pwa/routes/ratings.js +92 -32
- package/dist/pwa/routes/recover-key.js +66 -26
- package/dist/pwa/routes/referral.js +37 -52
- package/dist/pwa/routes/reputation.js +3 -2
- package/dist/pwa/routes/returns.js +76 -73
- package/dist/pwa/routes/reviews.js +41 -18
- package/dist/pwa/routes/rewards-apply.js +16 -15
- package/dist/pwa/routes/rewards-auto-downgrade.js +9 -7
- package/dist/pwa/routes/rewards-escrow-expire.js +7 -5
- package/dist/pwa/routes/rfqs.js +163 -85
- package/dist/pwa/routes/search.js +16 -14
- package/dist/pwa/routes/secondhand.js +25 -22
- package/dist/pwa/routes/seller-quota.js +24 -26
- package/dist/pwa/routes/share-redirects.js +60 -55
- package/dist/pwa/routes/shareables-interactions.js +34 -35
- package/dist/pwa/routes/shareables.js +55 -51
- package/dist/pwa/routes/shop-referral.js +58 -0
- package/dist/pwa/routes/shops.js +25 -20
- package/dist/pwa/routes/signaling.js +10 -9
- package/dist/pwa/routes/skill-market.js +16 -16
- package/dist/pwa/routes/skills.js +15 -14
- package/dist/pwa/routes/snf.js +14 -13
- package/dist/pwa/routes/tags.js +10 -9
- package/dist/pwa/routes/task-proposals.js +121 -0
- package/dist/pwa/routes/trial.js +72 -52
- package/dist/pwa/routes/trusted-kpi.js +20 -18
- package/dist/pwa/routes/url-claim.js +67 -28
- package/dist/pwa/routes/users-public.js +62 -70
- package/dist/pwa/routes/variants.js +12 -13
- package/dist/pwa/routes/verifier-user.js +61 -21
- package/dist/pwa/routes/verify-tasks.js +49 -25
- package/dist/pwa/routes/waitlist.js +16 -15
- package/dist/pwa/routes/wallet-read.js +75 -37
- package/dist/pwa/routes/wallet-write.js +12 -9
- package/dist/pwa/routes/webauthn.js +25 -26
- package/dist/pwa/routes/webhooks.js +26 -26
- package/dist/pwa/routes/welcome.js +45 -50
- package/dist/pwa/routes/wishlist-qa.js +29 -32
- package/dist/pwa/server.js +304 -90
- package/dist/version.js +1 -1
- package/package.json +76 -3
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { recordRatingReputation } from '../../layer4-economics/L4-3-reputation/reputation-engine.js';
|
|
2
|
+
import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
2
3
|
const RATING_BLIND_DAYS = 14;
|
|
3
4
|
function parseDim(v) {
|
|
4
5
|
const n = Number(v);
|
|
@@ -7,20 +8,20 @@ function parseDim(v) {
|
|
|
7
8
|
export function registerRatingsRoutes(app, deps) {
|
|
8
9
|
const { db, generateId, auth, isTrustedRole, errorRes, broadcastSystemEvent } = deps;
|
|
9
10
|
// buyer → seller 评价(一单一评,仅 completed 订单可评)
|
|
10
|
-
app.post('/api/orders/:order_id/rating', (req, res) => {
|
|
11
|
+
app.post('/api/orders/:order_id/rating', async (req, res) => {
|
|
11
12
|
const user = auth(req, res);
|
|
12
13
|
if (!user)
|
|
13
14
|
return;
|
|
14
15
|
if (isTrustedRole(user))
|
|
15
16
|
return void errorRes(res, 403, 'TRUSTED_ROLE_NO_TRADE', '受信角色无购物功能');
|
|
16
|
-
const order =
|
|
17
|
+
const order = await dbOne('SELECT id, buyer_id, seller_id, product_id, status FROM orders WHERE id = ?', [req.params.order_id]);
|
|
17
18
|
if (!order)
|
|
18
19
|
return void res.status(404).json({ error: '订单不存在' });
|
|
19
20
|
if (order.buyer_id !== user.id)
|
|
20
21
|
return void res.status(403).json({ error: '仅买家可评价' });
|
|
21
22
|
if (order.status !== 'completed')
|
|
22
23
|
return void res.status(400).json({ error: '订单完成后才能评价' });
|
|
23
|
-
const existing =
|
|
24
|
+
const existing = await dbOne('SELECT order_id FROM order_ratings WHERE order_id = ?', [order.id]);
|
|
24
25
|
if (existing)
|
|
25
26
|
return void res.status(400).json({ error: '已评价过,每单仅可评一次' });
|
|
26
27
|
const stars = Number(req.body?.stars);
|
|
@@ -55,18 +56,18 @@ export function registerRatingsRoutes(app, deps) {
|
|
|
55
56
|
res.json({ success: true });
|
|
56
57
|
});
|
|
57
58
|
// seller → buyer 反向评价
|
|
58
|
-
app.post('/api/orders/:order_id/buyer-rating', (req, res) => {
|
|
59
|
+
app.post('/api/orders/:order_id/buyer-rating', async (req, res) => {
|
|
59
60
|
const user = auth(req, res);
|
|
60
61
|
if (!user)
|
|
61
62
|
return;
|
|
62
|
-
const order =
|
|
63
|
+
const order = await dbOne('SELECT id, buyer_id, seller_id, status FROM orders WHERE id = ?', [req.params.order_id]);
|
|
63
64
|
if (!order)
|
|
64
65
|
return void res.status(404).json({ error: '订单不存在' });
|
|
65
66
|
if (order.seller_id !== user.id)
|
|
66
67
|
return void res.status(403).json({ error: '仅卖家可评价买家' });
|
|
67
68
|
if (order.status !== 'completed')
|
|
68
69
|
return void res.status(400).json({ error: '订单完成后才能评价' });
|
|
69
|
-
const existing =
|
|
70
|
+
const existing = await dbOne('SELECT order_id FROM buyer_ratings WHERE order_id = ?', [order.id]);
|
|
70
71
|
if (existing)
|
|
71
72
|
return void res.status(400).json({ error: '已评价过,每单仅可评一次' });
|
|
72
73
|
const stars = Number(req.body?.stars);
|
|
@@ -96,21 +97,21 @@ export function registerRatingsRoutes(app, deps) {
|
|
|
96
97
|
res.json({ success: true });
|
|
97
98
|
});
|
|
98
99
|
// 查 seller → buyer 评价(双盲遮蔽:buyer 看不到,除非自己也评过 OR 窗口到期)
|
|
99
|
-
app.get('/api/orders/:order_id/buyer-rating', (req, res) => {
|
|
100
|
+
app.get('/api/orders/:order_id/buyer-rating', async (req, res) => {
|
|
100
101
|
const user = auth(req, res);
|
|
101
102
|
if (!user)
|
|
102
103
|
return;
|
|
103
|
-
const order =
|
|
104
|
+
const order = await dbOne('SELECT buyer_id, seller_id FROM orders WHERE id = ?', [req.params.order_id]);
|
|
104
105
|
if (!order)
|
|
105
106
|
return void res.status(404).json({ error: '订单不存在' });
|
|
106
107
|
if (order.buyer_id !== user.id && order.seller_id !== user.id) {
|
|
107
108
|
return void res.status(403).json({ error: '无权查看' });
|
|
108
109
|
}
|
|
109
|
-
const br =
|
|
110
|
+
const br = await dbOne(`SELECT stars, comment, dim_payment_speed, dim_communication, dim_responsiveness, hidden_until, created_at FROM buyer_ratings WHERE order_id = ?`, [req.params.order_id]);
|
|
110
111
|
if (!br)
|
|
111
112
|
return void res.json({ item: null });
|
|
112
113
|
const isBuyerView = order.buyer_id === user.id;
|
|
113
|
-
const buyerAlsoRated = !!
|
|
114
|
+
const buyerAlsoRated = !!(await dbOne(`SELECT order_id FROM order_ratings WHERE order_id = ?`, [req.params.order_id]));
|
|
114
115
|
const blindExpired = br.hidden_until && new Date(br.hidden_until) < new Date();
|
|
115
116
|
if (isBuyerView && !buyerAlsoRated && !blindExpired) {
|
|
116
117
|
return void res.json({ item: { masked: true, hidden_until: br.hidden_until, reason: 'blind_until_both_or_expire' } });
|
|
@@ -118,50 +119,56 @@ export function registerRatingsRoutes(app, deps) {
|
|
|
118
119
|
res.json({ item: br });
|
|
119
120
|
});
|
|
120
121
|
// 查 buyer → seller 评价(双盲遮蔽:seller 视角同样)
|
|
121
|
-
app.get('/api/orders/:order_id/rating', (req, res) => {
|
|
122
|
+
app.get('/api/orders/:order_id/rating', async (req, res) => {
|
|
122
123
|
const user = auth(req, res);
|
|
123
124
|
if (!user)
|
|
124
125
|
return;
|
|
125
|
-
const order =
|
|
126
|
+
const order = await dbOne('SELECT buyer_id, seller_id FROM orders WHERE id = ?', [req.params.order_id]);
|
|
126
127
|
if (!order)
|
|
127
128
|
return void res.status(404).json({ error: '订单不存在' });
|
|
128
129
|
if (order.buyer_id !== user.id && order.seller_id !== user.id) {
|
|
129
130
|
return void res.status(403).json({ error: '无权查看' });
|
|
130
131
|
}
|
|
131
|
-
const r =
|
|
132
|
+
const r = await dbOne('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 = ?', [req.params.order_id]);
|
|
132
133
|
if (!r)
|
|
133
134
|
return void res.json({ item: null });
|
|
134
135
|
const isSellerView = order.seller_id === user.id;
|
|
135
|
-
const sellerAlsoRated = !!
|
|
136
|
+
const sellerAlsoRated = !!(await dbOne(`SELECT order_id FROM buyer_ratings WHERE order_id = ?`, [req.params.order_id]));
|
|
136
137
|
const blindExpired = r.hidden_until && new Date(r.hidden_until) < new Date();
|
|
137
138
|
if (isSellerView && !sellerAlsoRated && !blindExpired) {
|
|
138
139
|
return void res.json({ item: { masked: true, hidden_until: r.hidden_until, reason: 'blind_until_both_or_expire' } });
|
|
139
140
|
}
|
|
140
141
|
res.json({ item: r });
|
|
141
142
|
});
|
|
142
|
-
app.post('/api/orders/:order_id/rating/reply', (req, res) => {
|
|
143
|
+
app.post('/api/orders/:order_id/rating/reply', async (req, res) => {
|
|
143
144
|
const user = auth(req, res);
|
|
144
145
|
if (!user)
|
|
145
146
|
return;
|
|
146
|
-
const r =
|
|
147
|
+
const r = await dbOne('SELECT seller_id, reply, hidden_until FROM order_ratings WHERE order_id = ?', [req.params.order_id]);
|
|
147
148
|
if (!r)
|
|
148
149
|
return void res.status(404).json({ error: '该订单暂无评价' });
|
|
149
150
|
if (r.seller_id !== user.id)
|
|
150
151
|
return void res.status(403).json({ error: '仅卖家可回复' });
|
|
152
|
+
// 双盲铁律:未揭晓前不能回复(回复=已读到评价)。揭晓条件 = 自己也评过买家 OR 盲评期已过。
|
|
153
|
+
const sellerAlsoRated = !!(await dbOne(`SELECT order_id FROM buyer_ratings WHERE order_id = ?`, [req.params.order_id]));
|
|
154
|
+
const blindExpired = !!r.hidden_until && new Date(r.hidden_until) < new Date();
|
|
155
|
+
if (!sellerAlsoRated && !blindExpired) {
|
|
156
|
+
return void res.status(403).json({ error: '双盲期未结束:请先评价买家,或等盲评期满后再回应', error_code: 'RATING_STILL_BLIND' });
|
|
157
|
+
}
|
|
151
158
|
if (r.reply)
|
|
152
159
|
return void res.status(400).json({ error: '已回复过,每条评价仅可回复一次' });
|
|
153
160
|
const reply = req.body?.reply ? String(req.body.reply).slice(0, 500) : null;
|
|
154
161
|
if (!reply)
|
|
155
162
|
return void res.status(400).json({ error: '回复不能为空' });
|
|
156
|
-
|
|
163
|
+
await dbRun(`UPDATE order_ratings SET reply = ?, replied_at = datetime('now') WHERE order_id = ?`, [reply, req.params.order_id]);
|
|
157
164
|
res.json({ success: true });
|
|
158
165
|
});
|
|
159
166
|
// W3 买家追问 — 在卖家 reply 后可追问一次
|
|
160
|
-
app.post('/api/orders/:order_id/rating/followup', (req, res) => {
|
|
167
|
+
app.post('/api/orders/:order_id/rating/followup', async (req, res) => {
|
|
161
168
|
const user = auth(req, res);
|
|
162
169
|
if (!user)
|
|
163
170
|
return;
|
|
164
|
-
const r =
|
|
171
|
+
const r = await dbOne('SELECT buyer_id, reply, buyer_followup FROM order_ratings WHERE order_id = ?', [req.params.order_id]);
|
|
165
172
|
if (!r)
|
|
166
173
|
return void res.status(404).json({ error: '该订单暂无评价' });
|
|
167
174
|
if (r.buyer_id !== user.id)
|
|
@@ -173,15 +180,14 @@ export function registerRatingsRoutes(app, deps) {
|
|
|
173
180
|
const followup = req.body?.followup ? String(req.body.followup).trim().slice(0, 200) : '';
|
|
174
181
|
if (followup.length < 2)
|
|
175
182
|
return void res.status(400).json({ error: '追问内容至少 2 字' });
|
|
176
|
-
|
|
177
|
-
.run(followup, req.params.order_id);
|
|
183
|
+
await dbRun(`UPDATE order_ratings SET buyer_followup = ?, buyer_followup_at = datetime('now') WHERE order_id = ?`, [followup, req.params.order_id]);
|
|
178
184
|
res.json({ success: true });
|
|
179
185
|
});
|
|
180
186
|
// 公开:商品评价 + 聚合(仅展示双盲已揭晓的)
|
|
181
|
-
app.get('/api/products/:product_id/ratings', (req, res) => {
|
|
187
|
+
app.get('/api/products/:product_id/ratings', async (req, res) => {
|
|
182
188
|
const limit = Math.min(50, Math.max(1, Number(req.query.limit) || 20));
|
|
183
189
|
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 =
|
|
190
|
+
const rows = await dbAll(`
|
|
185
191
|
SELECT r.stars, r.comment, r.reply, r.replied_at, r.buyer_followup, r.buyer_followup_at, r.created_at,
|
|
186
192
|
r.dim_quality, r.dim_speed, r.dim_service,
|
|
187
193
|
u.name as buyer_name, u.handle as buyer_handle
|
|
@@ -189,8 +195,8 @@ export function registerRatingsRoutes(app, deps) {
|
|
|
189
195
|
JOIN users u ON u.id = r.buyer_id
|
|
190
196
|
WHERE r.product_id = ? AND ${blindOpen}
|
|
191
197
|
ORDER BY r.created_at DESC LIMIT ?
|
|
192
|
-
|
|
193
|
-
const agg =
|
|
198
|
+
`, [req.params.product_id, limit]);
|
|
199
|
+
const agg = await dbOne(`
|
|
194
200
|
SELECT COUNT(*) as cnt, COALESCE(AVG(stars), 0) as avg_stars,
|
|
195
201
|
SUM(CASE WHEN stars = 5 THEN 1 ELSE 0 END) as s5,
|
|
196
202
|
SUM(CASE WHEN stars = 4 THEN 1 ELSE 0 END) as s4,
|
|
@@ -198,23 +204,77 @@ export function registerRatingsRoutes(app, deps) {
|
|
|
198
204
|
SUM(CASE WHEN stars = 2 THEN 1 ELSE 0 END) as s2,
|
|
199
205
|
SUM(CASE WHEN stars = 1 THEN 1 ELSE 0 END) as s1
|
|
200
206
|
FROM order_ratings r WHERE product_id = ? AND ${blindOpen}
|
|
201
|
-
|
|
207
|
+
`, [req.params.product_id]);
|
|
202
208
|
res.json({ items: rows, agg });
|
|
203
209
|
});
|
|
204
|
-
//
|
|
205
|
-
|
|
210
|
+
// 卖家:自己店铺收到的全部评价(含 order_id 便于逐条回复 + 回复/追问状态)。
|
|
211
|
+
// 与公开聚合 endpoint 分开:authed + 只返回本人的评价 + 暴露 order_id(仅给卖家本人)。
|
|
212
|
+
// 纯只读,不改任何评价 / 资金逻辑;回复仍走既有 POST /orders/:order_id/rating/reply。
|
|
213
|
+
// ⚠️ 必须注册在 /api/sellers/:seller_id/ratings 【之前】,否则 'me' 会被 :seller_id 参数路由抢匹配。
|
|
214
|
+
app.get('/api/sellers/me/ratings', async (req, res) => {
|
|
215
|
+
const user = auth(req, res);
|
|
216
|
+
if (!user)
|
|
217
|
+
return;
|
|
218
|
+
const limit = Math.min(100, Math.max(1, Number(req.query.limit) || 50));
|
|
219
|
+
// 双盲铁律:卖家看 buyer→seller 评价,必须【自己也评过买家】(buyer_ratings 存在) 或【盲评期已过】(hidden_until 到期)。
|
|
220
|
+
// 否则只返回遮蔽行(不含 stars/comment/reply),与 GET /orders/:id/rating 的揭晓条件一致 —— 防卖家看了买家评价再反向报复。
|
|
221
|
+
const rows = await dbAll(`
|
|
222
|
+
SELECT r.order_id, r.stars, r.comment, r.reply, r.replied_at, r.buyer_followup, r.buyer_followup_at, r.created_at, r.product_id, r.hidden_until,
|
|
223
|
+
p.title as product_title,
|
|
224
|
+
u.name as buyer_name, u.handle as buyer_handle,
|
|
225
|
+
(SELECT 1 FROM buyer_ratings br WHERE br.order_id = r.order_id) AS seller_also_rated
|
|
226
|
+
FROM order_ratings r
|
|
227
|
+
JOIN products p ON p.id = r.product_id
|
|
228
|
+
JOIN users u ON u.id = r.buyer_id
|
|
229
|
+
WHERE r.seller_id = ?
|
|
230
|
+
ORDER BY r.created_at DESC LIMIT ?
|
|
231
|
+
`, [user.id, limit]);
|
|
232
|
+
const now = Date.now();
|
|
233
|
+
let unreplied = 0;
|
|
234
|
+
const items = rows.map(r => {
|
|
235
|
+
const blindExpired = !!r.hidden_until && new Date(r.hidden_until).getTime() < now;
|
|
236
|
+
const revealed = !!r.seller_also_rated || blindExpired;
|
|
237
|
+
if (!revealed) {
|
|
238
|
+
// 遮蔽:只回最小信息(有评价 + 商品 + 解除条件),绝不泄露分数/评论/回复
|
|
239
|
+
return { order_id: r.order_id, product_title: r.product_title, created_at: r.created_at, hidden_until: r.hidden_until, masked: true, reveal_reason: 'blind_until_both_or_expire' };
|
|
240
|
+
}
|
|
241
|
+
if (!r.reply)
|
|
242
|
+
unreplied++;
|
|
243
|
+
return {
|
|
244
|
+
order_id: r.order_id, stars: r.stars, comment: r.comment, reply: r.reply, replied_at: r.replied_at,
|
|
245
|
+
buyer_followup: r.buyer_followup, buyer_followup_at: r.buyer_followup_at, created_at: r.created_at,
|
|
246
|
+
product_id: r.product_id, product_title: r.product_title, buyer_name: r.buyer_name, buyer_handle: r.buyer_handle,
|
|
247
|
+
masked: false,
|
|
248
|
+
};
|
|
249
|
+
});
|
|
250
|
+
// 聚合双盲铁律:cnt / avg_stars 必须【只算已揭晓评价】,否则盲评期内卖家能从均分反推买家未揭晓评分。
|
|
251
|
+
// 与公开面同 blindOpen 条件;另回 masked_count(只告知"有多少条遮蔽中",不含分数)。
|
|
252
|
+
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'))`;
|
|
253
|
+
const agg = await dbOne(`
|
|
254
|
+
SELECT
|
|
255
|
+
SUM(CASE WHEN ${blindOpen} THEN 1 ELSE 0 END) as cnt,
|
|
256
|
+
COALESCE(AVG(CASE WHEN ${blindOpen} THEN stars END), 0) as avg_stars,
|
|
257
|
+
SUM(CASE WHEN ${blindOpen} THEN 0 ELSE 1 END) as masked_count
|
|
258
|
+
FROM order_ratings r WHERE r.seller_id = ?`, [user.id]);
|
|
259
|
+
res.json({ items, agg: { ...(agg || {}), unreplied } });
|
|
260
|
+
});
|
|
261
|
+
// 公开:卖家评价聚合(卖家主页)。注册在 /me 之后(见上面注释)。
|
|
262
|
+
app.get('/api/sellers/:seller_id/ratings', async (req, res) => {
|
|
206
263
|
const limit = Math.min(50, Math.max(1, Number(req.query.limit) || 20));
|
|
207
|
-
|
|
264
|
+
// 双盲铁律(公开面):只展示已揭晓的评价 —— 与 GET /products/:id/ratings 同条件。
|
|
265
|
+
// 揭晓 = 双方都评过(buyer_ratings 存在) OR 无盲评窗口(hidden_until 空) OR 盲评期已过。
|
|
266
|
+
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'))`;
|
|
267
|
+
const rows = await dbAll(`
|
|
208
268
|
SELECT r.stars, r.comment, r.reply, r.replied_at, r.buyer_followup, r.buyer_followup_at, r.created_at, r.product_id,
|
|
209
269
|
p.title as product_title,
|
|
210
270
|
u.name as buyer_name, u.handle as buyer_handle
|
|
211
271
|
FROM order_ratings r
|
|
212
272
|
JOIN products p ON p.id = r.product_id
|
|
213
273
|
JOIN users u ON u.id = r.buyer_id
|
|
214
|
-
WHERE r.seller_id = ?
|
|
274
|
+
WHERE r.seller_id = ? AND ${blindOpen}
|
|
215
275
|
ORDER BY r.created_at DESC LIMIT ?
|
|
216
|
-
|
|
217
|
-
const agg =
|
|
276
|
+
`, [req.params.seller_id, limit]);
|
|
277
|
+
const agg = await dbOne(`SELECT COUNT(*) as cnt, COALESCE(AVG(stars), 0) as avg_stars FROM order_ratings r WHERE r.seller_id = ? AND ${blindOpen}`, [req.params.seller_id]);
|
|
218
278
|
res.json({ items: rows, agg });
|
|
219
279
|
});
|
|
220
280
|
}
|
|
@@ -1,8 +1,18 @@
|
|
|
1
|
+
import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
export function registerRecoverKeyRoutes(app, deps) {
|
|
2
|
-
|
|
3
|
+
// db 已走 RFC-016 异步 seam(dbOne/dbAll/dbRun),不再直接用 deps.db
|
|
4
|
+
const { internalAuditorId, issueCode, findActiveCode, canDeliverCodes, emailDeliveryNotConfigured, hashPassword, CODE_TTL_MIN, MAX_CODE_ATTEMPTS } = deps;
|
|
5
|
+
// 账号标识解析 —— 与 /api/login 一致:@handle / handle(小写)优先,name 兜底。
|
|
6
|
+
// 找回三步全部复用它,否则用 handle 登录的用户(如 @holden)在找回页按 name 永远查不到、邮件不发。
|
|
7
|
+
const accountRef = (raw) => {
|
|
8
|
+
const display = String(raw || '').trim();
|
|
9
|
+
return { display, handleRef: display.replace(/^@/, '').toLowerCase() };
|
|
10
|
+
};
|
|
11
|
+
// (handle = ? OR name = ?) 子句 + 参数,排除 sys/auditor。
|
|
12
|
+
const ACCOUNT_MATCH = "(lower(coalesce(handle, '')) = ? OR name = ?) AND id NOT IN ('sys_protocol', ?)";
|
|
3
13
|
// IP 级速率(5/min)— 防爆破列举账户
|
|
4
14
|
const recoverKeyHits = new Map();
|
|
5
|
-
app.post('/api/recover-key', (req, res) => {
|
|
15
|
+
app.post('/api/recover-key', async (req, res) => {
|
|
6
16
|
const ip = req.ip || '';
|
|
7
17
|
if (ip) {
|
|
8
18
|
const now = Date.now();
|
|
@@ -23,10 +33,11 @@ export function registerRecoverKeyRoutes(app, deps) {
|
|
|
23
33
|
}
|
|
24
34
|
const { name } = req.body;
|
|
25
35
|
if (!name?.trim())
|
|
26
|
-
return void res.json({ error: '
|
|
27
|
-
const
|
|
36
|
+
return void res.json({ error: '请填写注册时使用的名称或 @用户名' });
|
|
37
|
+
const { display, handleRef } = accountRef(name);
|
|
38
|
+
const rows = await dbAll(`SELECT name, role, api_key, email, phone, created_at FROM users WHERE ${ACCOUNT_MATCH}`, [handleRef, display, internalAuditorId]);
|
|
28
39
|
if (rows.length === 0)
|
|
29
|
-
return void res.json({ error: '
|
|
40
|
+
return void res.json({ error: '未找到该名称 / @用户名的账号' });
|
|
30
41
|
const mask = (s) => s && s.length > 8 ? `${s.slice(0, 4)}…${s.slice(-4)}` : s;
|
|
31
42
|
const maskEmail = (e) => {
|
|
32
43
|
if (!e)
|
|
@@ -54,47 +65,76 @@ export function registerRecoverKeyRoutes(app, deps) {
|
|
|
54
65
|
});
|
|
55
66
|
});
|
|
56
67
|
// 步骤 1:发送验证码到已绑定邮箱(防泄露:找没找到都同响应)
|
|
57
|
-
app.post('/api/recover-key/start', (req, res) => {
|
|
68
|
+
app.post('/api/recover-key/start', async (req, res) => {
|
|
58
69
|
const { name, email } = req.body;
|
|
59
70
|
if (!name?.trim() || !email?.trim())
|
|
60
71
|
return void res.json({ error: '请填写名称和邮箱' });
|
|
72
|
+
if (!canDeliverCodes()) {
|
|
73
|
+
const unavailable = emailDeliveryNotConfigured();
|
|
74
|
+
return void res.status(unavailable.status).json({ error: unavailable.error, error_code: unavailable.error_code });
|
|
75
|
+
}
|
|
61
76
|
const target = email.trim().toLowerCase();
|
|
62
|
-
const
|
|
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({
|
|
77
|
+
const genericResponse = {
|
|
70
78
|
success: true,
|
|
71
79
|
notice: '若该名称与邮箱组合存在,验证码已发送至该邮箱',
|
|
72
80
|
expires_in_min: CODE_TTL_MIN,
|
|
73
|
-
}
|
|
81
|
+
};
|
|
82
|
+
const { display, handleRef } = accountRef(name);
|
|
83
|
+
const user = await dbOne(`
|
|
84
|
+
SELECT id, name, email FROM users
|
|
85
|
+
WHERE ${ACCOUNT_MATCH} AND email = ? AND email_verified = 1 LIMIT 1
|
|
86
|
+
`, [handleRef, display, internalAuditorId, target]);
|
|
87
|
+
if (user) {
|
|
88
|
+
const issued = await issueCode(user.id, 'email', target, 'recover_key');
|
|
89
|
+
if (!issued.ok) {
|
|
90
|
+
console.warn(`[recover-key] verification email delivery failed: ${issued.error_code}`);
|
|
91
|
+
return void res.json(genericResponse);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
res.json(genericResponse);
|
|
74
95
|
});
|
|
75
|
-
// 步骤 2:提交验证码 → 返回完整 api_key
|
|
76
|
-
|
|
77
|
-
|
|
96
|
+
// 步骤 2:提交验证码 → 返回完整 api_key,并可选同时重置登录密码(code + new_password)。
|
|
97
|
+
// 安全等价:本端点本就返回完整 api_key(最高凭证),允许同时重置密码不扩大权限面 —— 验证码已是同等门槛。
|
|
98
|
+
app.post('/api/recover-key/confirm', async (req, res) => {
|
|
99
|
+
const { name, email, code, new_password } = req.body;
|
|
78
100
|
if (!name?.trim() || !email?.trim() || !code?.trim())
|
|
79
101
|
return void res.json({ error: '请填写完整信息' });
|
|
102
|
+
// 可选新密码:格式与 change-password 一致(≥8,≤200)。先校验格式,失败【不消费验证码】,可重试。
|
|
103
|
+
const wantsPasswordReset = new_password !== undefined && new_password !== null && String(new_password) !== '';
|
|
104
|
+
if (wantsPasswordReset) {
|
|
105
|
+
if (String(new_password).length < 8)
|
|
106
|
+
return void res.json({ error: '新密码至少 8 字符' });
|
|
107
|
+
if (String(new_password).length > 200)
|
|
108
|
+
return void res.json({ error: '密码过长(>200 字符)' });
|
|
109
|
+
}
|
|
80
110
|
const target = email.trim().toLowerCase();
|
|
81
111
|
const row = findActiveCode('email', target, 'recover_key');
|
|
82
112
|
if (!row)
|
|
83
113
|
return void res.json({ error: '验证码已过期或未发送,请重新开始' });
|
|
84
|
-
const user =
|
|
85
|
-
|
|
86
|
-
|
|
114
|
+
const user = await dbOne(`SELECT id, name, handle, api_key FROM users WHERE id = ?`, [row.user_id]);
|
|
115
|
+
const { display, handleRef } = accountRef(name);
|
|
116
|
+
const refMatches = !!user && (user.name === display || String(user.handle || '').toLowerCase() === handleRef);
|
|
117
|
+
if (!user || !refMatches)
|
|
118
|
+
return void res.json({ error: '名称 / @用户名与验证码不匹配' });
|
|
87
119
|
if (String(row.code) !== code.trim()) {
|
|
88
120
|
const attempts = row.attempts + 1;
|
|
89
121
|
if (attempts >= MAX_CODE_ATTEMPTS) {
|
|
90
|
-
|
|
91
|
-
.run(attempts, row.id);
|
|
122
|
+
await dbRun("UPDATE verification_codes SET attempts = ?, used_at = datetime('now') WHERE id = ?", [attempts, row.id]);
|
|
92
123
|
return void res.json({ error: '错误次数过多,验证码已作废,请重新开始' });
|
|
93
124
|
}
|
|
94
|
-
|
|
125
|
+
await dbRun("UPDATE verification_codes SET attempts = ? WHERE id = ?", [attempts, row.id]);
|
|
95
126
|
return void res.json({ error: `验证码错误(剩余 ${MAX_CODE_ATTEMPTS - attempts} 次)` });
|
|
96
127
|
}
|
|
97
|
-
|
|
98
|
-
|
|
128
|
+
await dbRun("UPDATE verification_codes SET used_at = datetime('now') WHERE id = ?", [row.id]);
|
|
129
|
+
// optional password reset — same credential gate as returning the api_key, so no extra power.
|
|
130
|
+
let passwordReset = false;
|
|
131
|
+
if (wantsPasswordReset) {
|
|
132
|
+
// mirror /profile/set-password: also clear lock state, else a user who forgot + got locked out by
|
|
133
|
+
// failed attempts stays locked and "new password is correct but can't log in" (auth-login rejects
|
|
134
|
+
// locked users before verifying the password).
|
|
135
|
+
await dbRun("UPDATE users SET password_hash = ?, failed_attempts = 0, locked_until = NULL, updated_at = datetime('now') WHERE id = ?", [hashPassword(String(new_password)), user.id]);
|
|
136
|
+
passwordReset = true;
|
|
137
|
+
}
|
|
138
|
+
res.json({ success: true, api_key: user.api_key, name: user.name, ...(passwordReset ? { password_reset: true } : {}) });
|
|
99
139
|
});
|
|
100
140
|
}
|
|
@@ -1,28 +1,31 @@
|
|
|
1
|
+
import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
export function registerReferralRoutes(app, deps) {
|
|
2
|
-
|
|
3
|
+
// db 已全量走 RFC-016 异步 seam(dbOne/dbAll/dbRun),不再直接用 deps.db
|
|
4
|
+
const { auth, requireProtocolAdmin, logAdminAction, issueInviteSlot, inviteRotationLookup } = deps;
|
|
3
5
|
// B-1: 个人邀请 dashboard
|
|
4
|
-
app.get('/api/referral/me', (req, res) => {
|
|
6
|
+
app.get('/api/referral/me', async (req, res) => {
|
|
5
7
|
const user = auth(req, res);
|
|
6
8
|
if (!user)
|
|
7
9
|
return;
|
|
8
10
|
const code = user.permanent_code || null;
|
|
9
11
|
// 我直接邀请的人
|
|
10
|
-
const directInvitees =
|
|
12
|
+
const directInvitees = await dbAll(`
|
|
11
13
|
SELECT u.id, u.handle, u.name, u.role, u.created_at,
|
|
12
14
|
(SELECT COUNT(*) FROM orders WHERE buyer_id = u.id AND status = 'completed') as completed_orders,
|
|
13
15
|
(SELECT COALESCE(SUM(total_amount), 0) FROM orders WHERE buyer_id = u.id AND status = 'completed') as gmv
|
|
14
16
|
FROM users u WHERE u.sponsor_id = ?
|
|
15
17
|
ORDER BY u.created_at DESC LIMIT 50
|
|
16
|
-
|
|
18
|
+
`, [user.id]);
|
|
17
19
|
// 推土机奖励 / 商品分享佣金(commission_records 按订单粒度)
|
|
18
|
-
const earnings =
|
|
20
|
+
const earnings = (await dbOne(`
|
|
19
21
|
SELECT COUNT(*) as cnt, COALESCE(SUM(amount), 0) as total FROM commission_records WHERE beneficiary_id = ?
|
|
20
|
-
|
|
21
|
-
const todayEarnings =
|
|
22
|
-
const monthEarnings =
|
|
22
|
+
`, [user.id]));
|
|
23
|
+
const todayEarnings = (await dbOne(`SELECT COALESCE(SUM(amount), 0) as t FROM commission_records WHERE beneficiary_id = ? AND created_at > datetime('now', '-1 day')`, [user.id])).t;
|
|
24
|
+
const monthEarnings = (await dbOne(`SELECT COALESCE(SUM(amount), 0) as t FROM commission_records WHERE beneficiary_id = ? AND created_at > datetime('now', '-30 days')`, [user.id])).t;
|
|
23
25
|
res.json({
|
|
24
26
|
invite_code: code,
|
|
25
|
-
invite_link: code ? `${req.protocol}://${req.get('host')}
|
|
27
|
+
invite_link: code ? `${req.protocol}://${req.get('host')}/i/${code}` : null,
|
|
28
|
+
invite_unavailable_reason: code ? null : 'permanent_code_missing — refresh or contact support',
|
|
26
29
|
direct_invitees_count: directInvitees.length,
|
|
27
30
|
direct_invitees: directInvitees,
|
|
28
31
|
earnings: {
|
|
@@ -34,8 +37,8 @@ export function registerReferralRoutes(app, deps) {
|
|
|
34
37
|
});
|
|
35
38
|
});
|
|
36
39
|
// 公开邀请码轮询(开关 ON 时)
|
|
37
|
-
app.post('/api/invite/rotate', (_req, res) => {
|
|
38
|
-
const enabled =
|
|
40
|
+
app.post('/api/invite/rotate', async (_req, res) => {
|
|
41
|
+
const enabled = (await dbOne("SELECT value FROM system_state WHERE key='invite_rotation_enabled'"))?.value === '1';
|
|
39
42
|
if (!enabled)
|
|
40
43
|
return void res.status(403).json({ error: '邀请码获取暂未开放', enabled: false });
|
|
41
44
|
const slot = issueInviteSlot();
|
|
@@ -45,38 +48,37 @@ export function registerReferralRoutes(app, deps) {
|
|
|
45
48
|
res.json({ enabled: true, code: u.code });
|
|
46
49
|
});
|
|
47
50
|
// protocol 开关
|
|
48
|
-
app.post('/api/admin/invite-rotation/toggle', (req, res) => {
|
|
51
|
+
app.post('/api/admin/invite-rotation/toggle', async (req, res) => {
|
|
49
52
|
const admin = requireProtocolAdmin(req, res);
|
|
50
53
|
if (!admin)
|
|
51
54
|
return;
|
|
52
55
|
const { enabled } = req.body;
|
|
53
56
|
const v = enabled ? '1' : '0';
|
|
54
|
-
|
|
57
|
+
await dbRun("INSERT OR REPLACE INTO system_state (key, value) VALUES ('invite_rotation_enabled', ?)", [v]);
|
|
55
58
|
logAdminAction(admin.id, 'invite_rotation_toggle', 'system', 'invite_rotation_enabled', { value: v });
|
|
56
59
|
res.json({ success: true, enabled: !!enabled });
|
|
57
60
|
});
|
|
58
61
|
// RFC-003 #1122: 生成商品分享链接(把 MCP webaz_share_link 的本地计算搬到服务端,
|
|
59
62
|
// 让 MCP NETWORK 模式可代理)。RFC-002 §3.5 valuation-layer gate:需 rewards opt-in。
|
|
60
|
-
//
|
|
61
|
-
app.get('/api/share-link', (req, res) => {
|
|
63
|
+
// pre-public 去左右码:不再接受/返回 side,放置侧别由注册时系统自动决定。
|
|
64
|
+
app.get('/api/share-link', async (req, res) => {
|
|
62
65
|
const user = auth(req, res);
|
|
63
66
|
if (!user)
|
|
64
67
|
return;
|
|
65
68
|
const userId = user.id;
|
|
66
69
|
const productId = String(req.query.product_id || '');
|
|
67
|
-
const sideArg = String(req.query.side || 'auto');
|
|
68
70
|
if (!productId)
|
|
69
71
|
return void res.status(400).json({ error: 'product_id required', error_code: 'PRODUCT_ID_REQUIRED' });
|
|
70
|
-
const optIn =
|
|
72
|
+
const optIn = (await dbOne("SELECT rewards_opted_in FROM users WHERE id = ?", [userId]))?.rewards_opted_in ?? 0;
|
|
71
73
|
if (optIn !== 1) {
|
|
72
|
-
const getParam = (key, def) => {
|
|
73
|
-
const r =
|
|
74
|
+
const getParam = async (key, def) => {
|
|
75
|
+
const r = await dbOne("SELECT value FROM protocol_params WHERE key = ?", [key]);
|
|
74
76
|
return r ? Number(r.value) : def;
|
|
75
77
|
};
|
|
76
|
-
const minOrders = getParam('rewards_opt_in.min_completed_orders', 1);
|
|
77
|
-
const requirePasskey = getParam('rewards_opt_in.require_passkey', 1);
|
|
78
|
-
const totalCompleted =
|
|
79
|
-
const passkeyCount =
|
|
78
|
+
const minOrders = await getParam('rewards_opt_in.min_completed_orders', 1);
|
|
79
|
+
const requirePasskey = await getParam('rewards_opt_in.require_passkey', 1);
|
|
80
|
+
const totalCompleted = (await dbOne("SELECT COUNT(*) as n FROM orders WHERE buyer_id = ? AND status = 'completed'", [userId])).n;
|
|
81
|
+
const passkeyCount = (await dbOne("SELECT COUNT(*) as n FROM webauthn_credentials WHERE user_id = ?", [userId])).n;
|
|
80
82
|
const missing = [];
|
|
81
83
|
if (totalCompleted < minOrders)
|
|
82
84
|
missing.push(`completed_orders ${totalCompleted}/${minOrders}`);
|
|
@@ -86,50 +88,33 @@ export function registerReferralRoutes(app, deps) {
|
|
|
86
88
|
missing.push('application_not_submitted');
|
|
87
89
|
return void res.status(403).json({
|
|
88
90
|
error: 'rewards_opt_in_required',
|
|
89
|
-
message: 'Share-link generation is a valuation-layer action — requires
|
|
91
|
+
message: 'Share-link generation is a valuation-layer (rewards / share-link) action, NOT a contribution gate — requires rewards / share-commission opt-in (RFC-002 §3.5)',
|
|
90
92
|
missing_requirements: missing,
|
|
91
93
|
next_steps: [
|
|
92
|
-
'Open PWA #me → tap "
|
|
94
|
+
'Open PWA #me → tap "申请分享分润 / Enable share-commission opt-in"',
|
|
93
95
|
'Read the 8-second disclosure (cannot skip)',
|
|
94
96
|
'Submit application — pre-checks run server-side',
|
|
95
97
|
],
|
|
96
98
|
});
|
|
97
99
|
}
|
|
98
|
-
const product =
|
|
100
|
+
const product = await dbOne("SELECT id, title, price, commission_rate FROM products WHERE id = ? AND status='active'", [productId]);
|
|
99
101
|
if (!product)
|
|
100
102
|
return void res.status(404).json({ error: '商品不存在或已下架', error_code: 'PRODUCT_NOT_FOUND' });
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
else {
|
|
106
|
-
const u = db.prepare("SELECT placement_pref, total_left_pv, total_right_pv, left_count, right_count FROM users WHERE id = ?")
|
|
107
|
-
.get(userId);
|
|
108
|
-
const pref = u?.placement_pref || 'team_count';
|
|
109
|
-
if (pref === 'pv_count') {
|
|
110
|
-
const since = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000).toISOString().slice(0, 19).replace('T', ' ');
|
|
111
|
-
const w = db.prepare(`SELECT COALESCE(SUM(consumed_left_pv),0) AS l, COALESCE(SUM(consumed_right_pv),0) AS r
|
|
112
|
-
FROM binary_score_records WHERE user_id = ? AND created_at >= ?`)
|
|
113
|
-
.get(userId, since);
|
|
114
|
-
const leftPv = Number(u?.total_left_pv ?? 0) + Number(w.l);
|
|
115
|
-
const rightPv = Number(u?.total_right_pv ?? 0) + Number(w.r);
|
|
116
|
-
side = leftPv <= rightPv ? 'left' : 'right';
|
|
117
|
-
}
|
|
118
|
-
else {
|
|
119
|
-
side = (Number(u?.left_count ?? 0) <= Number(u?.right_count ?? 0)) ? 'left' : 'right';
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
const completed = db.prepare("SELECT COUNT(*) as n FROM orders WHERE buyer_id = ? AND status = 'completed'").get(userId).n;
|
|
123
|
-
const override = db.prepare("SELECT l1_share_override FROM users WHERE id = ?").get(userId)?.l1_share_override ?? 0;
|
|
103
|
+
// pre-public 去左右码:分享链接不再计算/携带 side(放置侧别由注册时系统自动决定)
|
|
104
|
+
const completed = (await dbOne("SELECT COUNT(*) as n FROM orders WHERE buyer_id = ? AND status = 'completed'", [userId])).n;
|
|
105
|
+
const override = (await dbOne("SELECT l1_share_override FROM users WHERE id = ?", [userId]))?.l1_share_override ?? 0;
|
|
124
106
|
const canL1 = override === 1 || (override === 0 && completed > 0);
|
|
125
107
|
const rate = Number(product.commission_rate ?? 0);
|
|
126
|
-
|
|
108
|
+
// share ref uses permanent_code ONLY — never the raw user_id; fail clearly if it's missing.
|
|
109
|
+
const refCode = (await dbOne("SELECT permanent_code FROM users WHERE id = ?", [userId]))?.permanent_code || null;
|
|
110
|
+
if (!refCode)
|
|
111
|
+
return void res.status(409).json({ error: '邀请码暂不可用,请刷新或联系支持', error_code: 'PERMANENT_CODE_MISSING' });
|
|
112
|
+
const link = `/?ref=${refCode}#order-product/${productId}`;
|
|
127
113
|
res.json({
|
|
128
114
|
product: { id: product.id, title: product.title, price: product.price, commission_rate: rate },
|
|
129
115
|
share_link: link,
|
|
130
116
|
full_url_hint: 'Prepend webaz.xyz (production) to get the absolute URL',
|
|
131
|
-
|
|
132
|
-
binary_explanation: `New user via this link → placed in your ${side === 'left' ? '🔵 left' : '🟢 right'} subtree (tail anchor)`,
|
|
117
|
+
placement_note: 'New user via this link → placement is recorded automatically by the system (no left/right choice).',
|
|
133
118
|
commission_eligibility: canL1
|
|
134
119
|
? `You will earn 3-tier commission: L1=${(rate * 0.70 * 100).toFixed(1)}% L2=${(rate * 0.20 * 100).toFixed(1)}% L3=${(rate * 0.10 * 100).toFixed(1)}% of sale price`
|
|
135
120
|
: 'You are NOT verified yet (need 1 completed purchase). 3-tier commission will be skipped, but points-matching still builds.',
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { dbOne } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
export function registerReputationRoutes(app, deps) {
|
|
2
3
|
const { db, auth, getReputation, getSellerMetrics } = deps;
|
|
3
4
|
app.get('/api/reputation', (req, res) => {
|
|
@@ -16,9 +17,9 @@ export function registerReputationRoutes(app, deps) {
|
|
|
16
17
|
metrics: getSellerMetrics(user.id),
|
|
17
18
|
});
|
|
18
19
|
});
|
|
19
|
-
app.get('/api/reputation/:userId', (req, res) => {
|
|
20
|
+
app.get('/api/reputation/:userId', async (req, res) => {
|
|
20
21
|
const rep = getReputation(db, req.params.userId);
|
|
21
|
-
const decayRow =
|
|
22
|
+
const decayRow = await dbOne(`SELECT last_decay_at FROM reputation_scores WHERE user_id = ?`, [req.params.userId]);
|
|
22
23
|
res.json({
|
|
23
24
|
level: rep.level,
|
|
24
25
|
total_points: rep.total_points,
|