@seasonkoh/webaz 0.1.23 → 0.1.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/layer0-foundation/L0-1-database/db-backends/pg-backend.js +51 -0
- package/dist/layer0-foundation/L0-1-database/db-backends/sql-dialect-datetime.js +437 -0
- package/dist/layer0-foundation/L0-1-database/db-backends/sql-placeholders.js +98 -0
- package/dist/layer0-foundation/L0-1-database/db.js +65 -0
- package/dist/layer0-foundation/L0-2-state-machine/order-chain.js +13 -11
- package/dist/layer0-foundation/L0-2-state-machine/transitions.js +1 -1
- package/dist/layer0-foundation/L0-5-manifest/manifest.js +13 -11
- package/dist/layer1-agent/L1-1-mcp-server/server.js +198 -83
- package/dist/layer1-agent/L1-2-external-anchor/anchor-engine.js +14 -12
- package/dist/layer2-business/L2-6-notifications/notification-engine.js +8 -5
- package/dist/layer2-business/L2-7-snf/snf-engine.js +16 -14
- package/dist/layer2-business/L2-8-feedback/build-feedback-engine.js +18 -10
- package/dist/layer2-business/L2-9-contribution/build-reputation-engine.js +37 -23
- package/dist/layer2-business/L2-9-contribution/build-task-agent-metadata-store.js +173 -0
- package/dist/layer2-business/L2-9-contribution/build-task-participation.js +47 -0
- package/dist/layer2-business/L2-9-contribution/build-task-read.js +222 -0
- package/dist/layer2-business/L2-9-contribution/build-tasks-engine.js +10 -2
- package/dist/layer2-business/L2-9-contribution/canonical-contribution-target.js +16 -0
- package/dist/layer2-business/L2-9-contribution/contribution-display-envelope.js +40 -0
- package/dist/layer2-business/L2-9-contribution/contribution-score-contract.js +36 -0
- package/dist/layer2-business/L2-9-contribution/contribution-score-evidence.js +61 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/canonical.js +60 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/github-credential.schema.js +140 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/github-fetch-adapter.js +437 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/self-consistency.js +38 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/verifier.js +231 -0
- package/dist/layer2-business/L2-9-contribution/github-credential-ingestion-engine.js +145 -0
- package/dist/layer2-business/L2-9-contribution/github-credential-store.js +115 -0
- package/dist/layer2-business/L2-9-contribution/identity-binding-engine.js +134 -0
- package/dist/layer2-business/L2-9-contribution/identity-binding-store.js +101 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-challenge-engine.js +126 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-challenge-store.js +30 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-engine.js +109 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-fact-precondition.js +22 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-proof-verifier.js +97 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-read.js +59 -0
- package/dist/layer2-business/L2-9-contribution/task-proposal-store.js +129 -0
- package/dist/layer2-business/L2-notes/note-photo-storage.js +4 -2
- package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +17 -15
- package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +11 -8
- package/dist/layer4-economics/L4-3-reputation/reputation-engine.js +9 -8
- package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +11 -8
- package/dist/layer4-economics/L4-4-skill-market/skill-listing-engine.js +22 -16
- package/dist/pwa/acp-feed.js +13 -1
- package/dist/pwa/contract-fingerprint.js +2 -0
- package/dist/pwa/endpoint-actions.js +5 -1
- package/dist/pwa/goal-index.js +8 -8
- package/dist/pwa/human-presence.js +62 -0
- package/dist/pwa/public/app.js +575 -68
- package/dist/pwa/public/i18n.js +29 -20
- package/dist/pwa/public/index.html +1 -0
- package/dist/pwa/public/openapi.json +2 -2
- package/dist/pwa/rate-limit.js +22 -0
- package/dist/pwa/routes/account-deletion.js +15 -13
- package/dist/pwa/routes/addresses.js +10 -9
- package/dist/pwa/routes/admin-admins.js +13 -14
- package/dist/pwa/routes/admin-analytics.js +109 -69
- package/dist/pwa/routes/admin-catalog.js +13 -11
- package/dist/pwa/routes/admin-editor-picks.js +15 -10
- package/dist/pwa/routes/admin-events.js +5 -3
- package/dist/pwa/routes/admin-health.js +2 -1
- package/dist/pwa/routes/admin-moderation.js +26 -29
- package/dist/pwa/routes/admin-ops.js +22 -21
- package/dist/pwa/routes/admin-protocol-params.js +16 -19
- package/dist/pwa/routes/admin-reports.js +23 -21
- package/dist/pwa/routes/admin-tokenomics.js +26 -25
- package/dist/pwa/routes/admin-users-lifecycle.js +37 -40
- package/dist/pwa/routes/admin-users-query.js +54 -53
- package/dist/pwa/routes/admin-verifier-flow.js +82 -41
- package/dist/pwa/routes/admin-verifier-whitelist.js +55 -27
- package/dist/pwa/routes/admin-wallet-ops.js +7 -5
- package/dist/pwa/routes/agent-buy.js +46 -22
- package/dist/pwa/routes/agent-governance.js +52 -56
- package/dist/pwa/routes/ai.js +7 -5
- package/dist/pwa/routes/analytics.js +43 -41
- package/dist/pwa/routes/anchors.js +19 -20
- package/dist/pwa/routes/announcements.js +13 -13
- package/dist/pwa/routes/arbitrator.js +97 -31
- package/dist/pwa/routes/auction.js +153 -114
- package/dist/pwa/routes/auth-login.js +6 -4
- package/dist/pwa/routes/auth-read.js +11 -9
- package/dist/pwa/routes/auth-register.js +35 -20
- package/dist/pwa/routes/auth-sessions.js +12 -11
- package/dist/pwa/routes/blocklist.js +16 -15
- package/dist/pwa/routes/build-feedback.js +10 -9
- package/dist/pwa/routes/build-reputation.js +6 -2
- package/dist/pwa/routes/build-tasks.js +45 -13
- package/dist/pwa/routes/buyer-feeds.js +27 -25
- package/dist/pwa/routes/cart.js +16 -15
- package/dist/pwa/routes/charity.js +212 -150
- package/dist/pwa/routes/chat.js +42 -43
- package/dist/pwa/routes/checkin-tasks.js +10 -9
- package/dist/pwa/routes/checkout-helpers.js +12 -10
- package/dist/pwa/routes/claim-initiators.js +34 -14
- package/dist/pwa/routes/claim-verify.js +86 -53
- package/dist/pwa/routes/claim-voting.js +43 -18
- package/dist/pwa/routes/contribution-identity.js +147 -0
- package/dist/pwa/routes/contribution-score.js +19 -0
- package/dist/pwa/routes/coupons.js +19 -16
- package/dist/pwa/routes/dashboards.js +18 -16
- package/dist/pwa/routes/dispute-cases.js +25 -24
- package/dist/pwa/routes/disputes-read.js +45 -51
- package/dist/pwa/routes/disputes-write.js +124 -61
- package/dist/pwa/routes/evidence.js +9 -9
- package/dist/pwa/routes/external-anchors.js +13 -12
- package/dist/pwa/routes/feedback.js +29 -33
- package/dist/pwa/routes/flash-sales.js +18 -16
- package/dist/pwa/routes/follows.js +25 -24
- package/dist/pwa/routes/governance-auto-deactivate.js +21 -9
- package/dist/pwa/routes/governance-onboarding.js +70 -59
- package/dist/pwa/routes/group-buys.js +22 -22
- package/dist/pwa/routes/growth.js +33 -30
- package/dist/pwa/routes/import-product.js +12 -10
- package/dist/pwa/routes/kyc.js +9 -8
- package/dist/pwa/routes/leaderboard.js +20 -18
- package/dist/pwa/routes/listings.js +23 -22
- package/dist/pwa/routes/logistics.js +10 -8
- package/dist/pwa/routes/manifests.js +27 -27
- package/dist/pwa/routes/me-data.js +23 -21
- package/dist/pwa/routes/notifications.js +7 -6
- package/dist/pwa/routes/offers.js +30 -12
- package/dist/pwa/routes/orders-action.js +33 -17
- package/dist/pwa/routes/orders-create.js +75 -20
- package/dist/pwa/routes/orders-read.js +21 -20
- package/dist/pwa/routes/p2p-products.js +30 -18
- package/dist/pwa/routes/payments-governance.js +61 -56
- package/dist/pwa/routes/peers.js +9 -8
- package/dist/pwa/routes/pin-receipts.js +13 -13
- package/dist/pwa/routes/products-aliases.js +12 -10
- package/dist/pwa/routes/products-claims.js +36 -17
- package/dist/pwa/routes/products-create.js +53 -38
- package/dist/pwa/routes/products-crud.js +17 -16
- package/dist/pwa/routes/products-links.js +49 -26
- package/dist/pwa/routes/products-list.js +6 -4
- package/dist/pwa/routes/products-meta.js +40 -39
- package/dist/pwa/routes/products-update.js +19 -5
- package/dist/pwa/routes/profile-credentials.js +14 -16
- package/dist/pwa/routes/profile-identity.js +14 -13
- package/dist/pwa/routes/profile-location.js +7 -6
- package/dist/pwa/routes/profile-placement.js +19 -17
- package/dist/pwa/routes/profile-prefs.js +11 -11
- package/dist/pwa/routes/promoter.js +55 -49
- package/dist/pwa/routes/public-build-tasks.js +19 -0
- package/dist/pwa/routes/public-utils.js +108 -46
- package/dist/pwa/routes/push.js +16 -15
- package/dist/pwa/routes/ratings.js +30 -30
- package/dist/pwa/routes/recover-key.js +13 -12
- package/dist/pwa/routes/referral.js +37 -32
- package/dist/pwa/routes/reputation.js +3 -2
- package/dist/pwa/routes/returns.js +76 -73
- package/dist/pwa/routes/reviews.js +41 -18
- package/dist/pwa/routes/rewards-apply.js +16 -15
- package/dist/pwa/routes/rewards-auto-downgrade.js +9 -7
- package/dist/pwa/routes/rewards-escrow-expire.js +7 -5
- package/dist/pwa/routes/rfqs.js +163 -85
- package/dist/pwa/routes/search.js +16 -14
- package/dist/pwa/routes/secondhand.js +25 -22
- package/dist/pwa/routes/seller-quota.js +24 -26
- package/dist/pwa/routes/share-redirects.js +59 -55
- package/dist/pwa/routes/shareables-interactions.js +34 -35
- package/dist/pwa/routes/shareables.js +55 -51
- package/dist/pwa/routes/shop-referral.js +57 -0
- package/dist/pwa/routes/shops.js +20 -18
- package/dist/pwa/routes/signaling.js +10 -9
- package/dist/pwa/routes/skill-market.js +16 -16
- package/dist/pwa/routes/skills.js +15 -14
- package/dist/pwa/routes/snf.js +14 -13
- package/dist/pwa/routes/tags.js +10 -9
- package/dist/pwa/routes/task-proposals.js +45 -0
- package/dist/pwa/routes/trial.js +69 -51
- package/dist/pwa/routes/trusted-kpi.js +20 -18
- package/dist/pwa/routes/url-claim.js +67 -28
- package/dist/pwa/routes/users-public.js +62 -60
- package/dist/pwa/routes/variants.js +12 -13
- package/dist/pwa/routes/verifier-user.js +61 -21
- package/dist/pwa/routes/verify-tasks.js +49 -25
- package/dist/pwa/routes/waitlist.js +16 -15
- package/dist/pwa/routes/wallet-read.js +74 -36
- package/dist/pwa/routes/wallet-write.js +12 -9
- package/dist/pwa/routes/webauthn.js +25 -26
- package/dist/pwa/routes/webhooks.js +26 -26
- package/dist/pwa/routes/welcome.js +45 -50
- package/dist/pwa/routes/wishlist-qa.js +29 -32
- package/dist/pwa/server.js +237 -81
- package/dist/version.js +1 -1
- package/package.json +47 -2
|
@@ -1,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,32 +119,32 @@ 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 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)
|
|
@@ -153,15 +154,15 @@ export function registerRatingsRoutes(app, deps) {
|
|
|
153
154
|
const reply = req.body?.reply ? String(req.body.reply).slice(0, 500) : null;
|
|
154
155
|
if (!reply)
|
|
155
156
|
return void res.status(400).json({ error: '回复不能为空' });
|
|
156
|
-
|
|
157
|
+
await dbRun(`UPDATE order_ratings SET reply = ?, replied_at = datetime('now') WHERE order_id = ?`, [reply, req.params.order_id]);
|
|
157
158
|
res.json({ success: true });
|
|
158
159
|
});
|
|
159
160
|
// W3 买家追问 — 在卖家 reply 后可追问一次
|
|
160
|
-
app.post('/api/orders/:order_id/rating/followup', (req, res) => {
|
|
161
|
+
app.post('/api/orders/:order_id/rating/followup', async (req, res) => {
|
|
161
162
|
const user = auth(req, res);
|
|
162
163
|
if (!user)
|
|
163
164
|
return;
|
|
164
|
-
const r =
|
|
165
|
+
const r = await dbOne('SELECT buyer_id, reply, buyer_followup FROM order_ratings WHERE order_id = ?', [req.params.order_id]);
|
|
165
166
|
if (!r)
|
|
166
167
|
return void res.status(404).json({ error: '该订单暂无评价' });
|
|
167
168
|
if (r.buyer_id !== user.id)
|
|
@@ -173,15 +174,14 @@ export function registerRatingsRoutes(app, deps) {
|
|
|
173
174
|
const followup = req.body?.followup ? String(req.body.followup).trim().slice(0, 200) : '';
|
|
174
175
|
if (followup.length < 2)
|
|
175
176
|
return void res.status(400).json({ error: '追问内容至少 2 字' });
|
|
176
|
-
|
|
177
|
-
.run(followup, req.params.order_id);
|
|
177
|
+
await dbRun(`UPDATE order_ratings SET buyer_followup = ?, buyer_followup_at = datetime('now') WHERE order_id = ?`, [followup, req.params.order_id]);
|
|
178
178
|
res.json({ success: true });
|
|
179
179
|
});
|
|
180
180
|
// 公开:商品评价 + 聚合(仅展示双盲已揭晓的)
|
|
181
|
-
app.get('/api/products/:product_id/ratings', (req, res) => {
|
|
181
|
+
app.get('/api/products/:product_id/ratings', async (req, res) => {
|
|
182
182
|
const limit = Math.min(50, Math.max(1, Number(req.query.limit) || 20));
|
|
183
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 =
|
|
184
|
+
const rows = await dbAll(`
|
|
185
185
|
SELECT r.stars, r.comment, r.reply, r.replied_at, r.buyer_followup, r.buyer_followup_at, r.created_at,
|
|
186
186
|
r.dim_quality, r.dim_speed, r.dim_service,
|
|
187
187
|
u.name as buyer_name, u.handle as buyer_handle
|
|
@@ -189,8 +189,8 @@ export function registerRatingsRoutes(app, deps) {
|
|
|
189
189
|
JOIN users u ON u.id = r.buyer_id
|
|
190
190
|
WHERE r.product_id = ? AND ${blindOpen}
|
|
191
191
|
ORDER BY r.created_at DESC LIMIT ?
|
|
192
|
-
|
|
193
|
-
const agg =
|
|
192
|
+
`, [req.params.product_id, limit]);
|
|
193
|
+
const agg = await dbOne(`
|
|
194
194
|
SELECT COUNT(*) as cnt, COALESCE(AVG(stars), 0) as avg_stars,
|
|
195
195
|
SUM(CASE WHEN stars = 5 THEN 1 ELSE 0 END) as s5,
|
|
196
196
|
SUM(CASE WHEN stars = 4 THEN 1 ELSE 0 END) as s4,
|
|
@@ -198,13 +198,13 @@ export function registerRatingsRoutes(app, deps) {
|
|
|
198
198
|
SUM(CASE WHEN stars = 2 THEN 1 ELSE 0 END) as s2,
|
|
199
199
|
SUM(CASE WHEN stars = 1 THEN 1 ELSE 0 END) as s1
|
|
200
200
|
FROM order_ratings r WHERE product_id = ? AND ${blindOpen}
|
|
201
|
-
|
|
201
|
+
`, [req.params.product_id]);
|
|
202
202
|
res.json({ items: rows, agg });
|
|
203
203
|
});
|
|
204
204
|
// 公开:卖家评价聚合(卖家主页)
|
|
205
|
-
app.get('/api/sellers/:seller_id/ratings', (req, res) => {
|
|
205
|
+
app.get('/api/sellers/:seller_id/ratings', async (req, res) => {
|
|
206
206
|
const limit = Math.min(50, Math.max(1, Number(req.query.limit) || 20));
|
|
207
|
-
const rows =
|
|
207
|
+
const rows = await dbAll(`
|
|
208
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
209
|
p.title as product_title,
|
|
210
210
|
u.name as buyer_name, u.handle as buyer_handle
|
|
@@ -213,8 +213,8 @@ export function registerRatingsRoutes(app, deps) {
|
|
|
213
213
|
JOIN users u ON u.id = r.buyer_id
|
|
214
214
|
WHERE r.seller_id = ?
|
|
215
215
|
ORDER BY r.created_at DESC LIMIT ?
|
|
216
|
-
|
|
217
|
-
const agg =
|
|
216
|
+
`, [req.params.seller_id, limit]);
|
|
217
|
+
const agg = await dbOne(`SELECT COUNT(*) as cnt, COALESCE(AVG(stars), 0) as avg_stars FROM order_ratings WHERE seller_id = ?`, [req.params.seller_id]);
|
|
218
218
|
res.json({ items: rows, agg });
|
|
219
219
|
});
|
|
220
220
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
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, CODE_TTL_MIN, MAX_CODE_ATTEMPTS } = deps;
|
|
3
5
|
// IP 级速率(5/min)— 防爆破列举账户
|
|
4
6
|
const recoverKeyHits = new Map();
|
|
5
|
-
app.post('/api/recover-key', (req, res) => {
|
|
7
|
+
app.post('/api/recover-key', async (req, res) => {
|
|
6
8
|
const ip = req.ip || '';
|
|
7
9
|
if (ip) {
|
|
8
10
|
const now = Date.now();
|
|
@@ -24,7 +26,7 @@ export function registerRecoverKeyRoutes(app, deps) {
|
|
|
24
26
|
const { name } = req.body;
|
|
25
27
|
if (!name?.trim())
|
|
26
28
|
return void res.json({ error: '请填写注册时使用的名称' });
|
|
27
|
-
const rows =
|
|
29
|
+
const rows = await dbAll("SELECT name, role, api_key, email, phone, created_at FROM users WHERE name = ? AND id NOT IN ('sys_protocol', ?)", [name.trim(), internalAuditorId]);
|
|
28
30
|
if (rows.length === 0)
|
|
29
31
|
return void res.json({ error: '未找到该名称的账号' });
|
|
30
32
|
const mask = (s) => s && s.length > 8 ? `${s.slice(0, 4)}…${s.slice(-4)}` : s;
|
|
@@ -54,16 +56,16 @@ export function registerRecoverKeyRoutes(app, deps) {
|
|
|
54
56
|
});
|
|
55
57
|
});
|
|
56
58
|
// 步骤 1:发送验证码到已绑定邮箱(防泄露:找没找到都同响应)
|
|
57
|
-
app.post('/api/recover-key/start', (req, res) => {
|
|
59
|
+
app.post('/api/recover-key/start', async (req, res) => {
|
|
58
60
|
const { name, email } = req.body;
|
|
59
61
|
if (!name?.trim() || !email?.trim())
|
|
60
62
|
return void res.json({ error: '请填写名称和邮箱' });
|
|
61
63
|
const target = email.trim().toLowerCase();
|
|
62
|
-
const user =
|
|
64
|
+
const user = await dbOne(`
|
|
63
65
|
SELECT id, name, email FROM users
|
|
64
66
|
WHERE name = ? AND email = ? AND email_verified = 1
|
|
65
67
|
AND id NOT IN ('sys_protocol', ?) LIMIT 1
|
|
66
|
-
|
|
68
|
+
`, [name.trim(), target, internalAuditorId]);
|
|
67
69
|
if (user)
|
|
68
70
|
issueCode(user.id, 'email', target, 'recover_key');
|
|
69
71
|
res.json({
|
|
@@ -73,7 +75,7 @@ export function registerRecoverKeyRoutes(app, deps) {
|
|
|
73
75
|
});
|
|
74
76
|
});
|
|
75
77
|
// 步骤 2:提交验证码 → 返回完整 api_key
|
|
76
|
-
app.post('/api/recover-key/confirm', (req, res) => {
|
|
78
|
+
app.post('/api/recover-key/confirm', async (req, res) => {
|
|
77
79
|
const { name, email, code } = req.body;
|
|
78
80
|
if (!name?.trim() || !email?.trim() || !code?.trim())
|
|
79
81
|
return void res.json({ error: '请填写完整信息' });
|
|
@@ -81,20 +83,19 @@ export function registerRecoverKeyRoutes(app, deps) {
|
|
|
81
83
|
const row = findActiveCode('email', target, 'recover_key');
|
|
82
84
|
if (!row)
|
|
83
85
|
return void res.json({ error: '验证码已过期或未发送,请重新开始' });
|
|
84
|
-
const user =
|
|
86
|
+
const user = await dbOne(`SELECT id, name, api_key FROM users WHERE id = ?`, [row.user_id]);
|
|
85
87
|
if (!user || user.name !== name.trim())
|
|
86
88
|
return void res.json({ error: '名称与验证码不匹配' });
|
|
87
89
|
if (String(row.code) !== code.trim()) {
|
|
88
90
|
const attempts = row.attempts + 1;
|
|
89
91
|
if (attempts >= MAX_CODE_ATTEMPTS) {
|
|
90
|
-
|
|
91
|
-
.run(attempts, row.id);
|
|
92
|
+
await dbRun("UPDATE verification_codes SET attempts = ?, used_at = datetime('now') WHERE id = ?", [attempts, row.id]);
|
|
92
93
|
return void res.json({ error: '错误次数过多,验证码已作废,请重新开始' });
|
|
93
94
|
}
|
|
94
|
-
|
|
95
|
+
await dbRun("UPDATE verification_codes SET attempts = ? WHERE id = ?", [attempts, row.id]);
|
|
95
96
|
return void res.json({ error: `验证码错误(剩余 ${MAX_CODE_ATTEMPTS - attempts} 次)` });
|
|
96
97
|
}
|
|
97
|
-
|
|
98
|
+
await dbRun("UPDATE verification_codes SET used_at = datetime('now') WHERE id = ?", [row.id]);
|
|
98
99
|
res.json({ success: true, api_key: user.api_key, name: user.name });
|
|
99
100
|
});
|
|
100
101
|
}
|
|
@@ -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,20 +48,20 @@ 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
63
|
// 与 PWA pickPreferredSide 对齐的 side 选择(team_count | pv_count)。
|
|
61
|
-
app.get('/api/share-link', (req, res) => {
|
|
64
|
+
app.get('/api/share-link', async (req, res) => {
|
|
62
65
|
const user = auth(req, res);
|
|
63
66
|
if (!user)
|
|
64
67
|
return;
|
|
@@ -67,16 +70,16 @@ export function registerReferralRoutes(app, deps) {
|
|
|
67
70
|
const sideArg = String(req.query.side || 'auto');
|
|
68
71
|
if (!productId)
|
|
69
72
|
return void res.status(400).json({ error: 'product_id required', error_code: 'PRODUCT_ID_REQUIRED' });
|
|
70
|
-
const optIn =
|
|
73
|
+
const optIn = (await dbOne("SELECT rewards_opted_in FROM users WHERE id = ?", [userId]))?.rewards_opted_in ?? 0;
|
|
71
74
|
if (optIn !== 1) {
|
|
72
|
-
const getParam = (key, def) => {
|
|
73
|
-
const r =
|
|
75
|
+
const getParam = async (key, def) => {
|
|
76
|
+
const r = await dbOne("SELECT value FROM protocol_params WHERE key = ?", [key]);
|
|
74
77
|
return r ? Number(r.value) : def;
|
|
75
78
|
};
|
|
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 =
|
|
79
|
+
const minOrders = await getParam('rewards_opt_in.min_completed_orders', 1);
|
|
80
|
+
const requirePasskey = await getParam('rewards_opt_in.require_passkey', 1);
|
|
81
|
+
const totalCompleted = (await dbOne("SELECT COUNT(*) as n FROM orders WHERE buyer_id = ? AND status = 'completed'", [userId])).n;
|
|
82
|
+
const passkeyCount = (await dbOne("SELECT COUNT(*) as n FROM webauthn_credentials WHERE user_id = ?", [userId])).n;
|
|
80
83
|
const missing = [];
|
|
81
84
|
if (totalCompleted < minOrders)
|
|
82
85
|
missing.push(`completed_orders ${totalCompleted}/${minOrders}`);
|
|
@@ -86,16 +89,16 @@ export function registerReferralRoutes(app, deps) {
|
|
|
86
89
|
missing.push('application_not_submitted');
|
|
87
90
|
return void res.status(403).json({
|
|
88
91
|
error: 'rewards_opt_in_required',
|
|
89
|
-
message: 'Share-link generation is a valuation-layer action — requires
|
|
92
|
+
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
93
|
missing_requirements: missing,
|
|
91
94
|
next_steps: [
|
|
92
|
-
'Open PWA #me → tap "
|
|
95
|
+
'Open PWA #me → tap "申请分享分润 / Enable share-commission opt-in"',
|
|
93
96
|
'Read the 8-second disclosure (cannot skip)',
|
|
94
97
|
'Submit application — pre-checks run server-side',
|
|
95
98
|
],
|
|
96
99
|
});
|
|
97
100
|
}
|
|
98
|
-
const product =
|
|
101
|
+
const product = await dbOne("SELECT id, title, price, commission_rate FROM products WHERE id = ? AND status='active'", [productId]);
|
|
99
102
|
if (!product)
|
|
100
103
|
return void res.status(404).json({ error: '商品不存在或已下架', error_code: 'PRODUCT_NOT_FOUND' });
|
|
101
104
|
let side = 'right';
|
|
@@ -103,14 +106,12 @@ export function registerReferralRoutes(app, deps) {
|
|
|
103
106
|
side = sideArg;
|
|
104
107
|
}
|
|
105
108
|
else {
|
|
106
|
-
const u =
|
|
107
|
-
.get(userId);
|
|
109
|
+
const u = await dbOne("SELECT placement_pref, total_left_pv, total_right_pv, left_count, right_count FROM users WHERE id = ?", [userId]);
|
|
108
110
|
const pref = u?.placement_pref || 'team_count';
|
|
109
111
|
if (pref === 'pv_count') {
|
|
110
112
|
const since = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000).toISOString().slice(0, 19).replace('T', ' ');
|
|
111
|
-
const w =
|
|
112
|
-
FROM binary_score_records WHERE user_id = ? AND created_at >=
|
|
113
|
-
.get(userId, since);
|
|
113
|
+
const w = (await dbOne(`SELECT COALESCE(SUM(consumed_left_pv),0) AS l, COALESCE(SUM(consumed_right_pv),0) AS r
|
|
114
|
+
FROM binary_score_records WHERE user_id = ? AND created_at >= ?`, [userId, since]));
|
|
114
115
|
const leftPv = Number(u?.total_left_pv ?? 0) + Number(w.l);
|
|
115
116
|
const rightPv = Number(u?.total_right_pv ?? 0) + Number(w.r);
|
|
116
117
|
side = leftPv <= rightPv ? 'left' : 'right';
|
|
@@ -119,11 +120,15 @@ export function registerReferralRoutes(app, deps) {
|
|
|
119
120
|
side = (Number(u?.left_count ?? 0) <= Number(u?.right_count ?? 0)) ? 'left' : 'right';
|
|
120
121
|
}
|
|
121
122
|
}
|
|
122
|
-
const completed =
|
|
123
|
-
const override =
|
|
123
|
+
const completed = (await dbOne("SELECT COUNT(*) as n FROM orders WHERE buyer_id = ? AND status = 'completed'", [userId])).n;
|
|
124
|
+
const override = (await dbOne("SELECT l1_share_override FROM users WHERE id = ?", [userId]))?.l1_share_override ?? 0;
|
|
124
125
|
const canL1 = override === 1 || (override === 0 && completed > 0);
|
|
125
126
|
const rate = Number(product.commission_rate ?? 0);
|
|
126
|
-
|
|
127
|
+
// share ref uses permanent_code ONLY — never the raw user_id; fail clearly if it's missing.
|
|
128
|
+
const refCode = (await dbOne("SELECT permanent_code FROM users WHERE id = ?", [userId]))?.permanent_code || null;
|
|
129
|
+
if (!refCode)
|
|
130
|
+
return void res.status(409).json({ error: '邀请码暂不可用,请刷新或联系支持', error_code: 'PERMANENT_CODE_MISSING' });
|
|
131
|
+
const link = `/?ref=${refCode}&side=${side}#order-product/${productId}`;
|
|
127
132
|
res.json({
|
|
128
133
|
product: { id: product.id, title: product.title, price: product.price, commission_rate: rate },
|
|
129
134
|
share_link: link,
|
|
@@ -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,
|